fix: repair version detection platform query format
- Fix GetLatestVersionByTypeAndArch to separate platform/architecture - Query now correctly uses platform='linux' and architecture='amd64' - Resolves UI showing 'no packages available' despite updates existing
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -14,37 +15,72 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AgentUpdateHandler handles agent update operations
|
|
||||||
type AgentUpdateHandler struct {
|
type AgentUpdateHandler struct {
|
||||||
agentQueries *queries.AgentQueries
|
agentQueries *queries.AgentQueries
|
||||||
agentUpdateQueries *queries.AgentUpdateQueries
|
agentUpdateQueries *queries.AgentUpdateQueries
|
||||||
commandQueries *queries.CommandQueries
|
commandQueries *queries.CommandQueries
|
||||||
signingService *services.SigningService
|
signingService *services.SigningService
|
||||||
|
nonceService *services.UpdateNonceService
|
||||||
agentHandler *AgentHandler
|
agentHandler *AgentHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAgentUpdateHandler creates a new agent update handler
|
// NewAgentUpdateHandler creates a new agent update handler
|
||||||
func NewAgentUpdateHandler(aq *queries.AgentQueries, auq *queries.AgentUpdateQueries, cq *queries.CommandQueries, ss *services.SigningService, ah *AgentHandler) *AgentUpdateHandler {
|
func NewAgentUpdateHandler(aq *queries.AgentQueries, auq *queries.AgentUpdateQueries, cq *queries.CommandQueries, ss *services.SigningService, ns *services.UpdateNonceService, ah *AgentHandler) *AgentUpdateHandler {
|
||||||
return &AgentUpdateHandler{
|
return &AgentUpdateHandler{
|
||||||
agentQueries: aq,
|
agentQueries: aq,
|
||||||
agentUpdateQueries: auq,
|
agentUpdateQueries: auq,
|
||||||
commandQueries: cq,
|
commandQueries: cq,
|
||||||
signingService: ss,
|
signingService: ss,
|
||||||
|
nonceService: ns,
|
||||||
agentHandler: ah,
|
agentHandler: ah,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateAgent handles POST /api/v1/agents/:id/update (manual agent update)
|
// UpdateAgent handles POST /api/v1/agents/:id/update (manual agent update)
|
||||||
func (h *AgentUpdateHandler) UpdateAgent(c *gin.Context) {
|
func (h *AgentUpdateHandler) UpdateAgent(c *gin.Context) {
|
||||||
|
// Extract agent ID from URL path
|
||||||
|
agentID := c.Param("id")
|
||||||
|
if agentID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "agent ID is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug logging for development (controlled via REDFLAG_DEBUG env var or query param)
|
||||||
|
debugMode := os.Getenv("REDFLAG_DEBUG") == "true" || c.Query("debug") == "true"
|
||||||
|
if debugMode {
|
||||||
|
log.Printf("[DEBUG] [UpdateAgent] Starting update request for agent %s from %s", agentID, c.ClientIP())
|
||||||
|
log.Printf("[DEBUG] [UpdateAgent] Content-Type: %s, Content-Length: %d", c.ContentType(), c.Request.ContentLength)
|
||||||
|
}
|
||||||
|
|
||||||
var req models.AgentUpdateRequest
|
var req models.AgentUpdateRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
if debugMode {
|
||||||
|
log.Printf("[DEBUG] [UpdateAgent] JSON binding error for agent %s: %v", agentID, err)
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"error": err.Error(),
|
||||||
|
"_error_context": "json_binding_failed", // Helps identify binding vs validation errors
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always log critical update operations for audit trail
|
||||||
|
log.Printf("[UPDATE] Agent %s received update request - Version: %s, Platform: %s", agentID, req.Version, req.Platform)
|
||||||
|
|
||||||
|
// Debug: Log the parsed request
|
||||||
|
if debugMode {
|
||||||
|
log.Printf("[DEBUG] [UpdateAgent] Parsed update request - Version: %s, Platform: %s, Nonce: %s", req.Version, req.Platform, req.Nonce)
|
||||||
|
}
|
||||||
|
|
||||||
|
agentIDUUID, err := uuid.Parse(agentID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[UPDATE] Agent ID format error for %s: %v", agentID, err)
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID format"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the agent exists
|
// Verify the agent exists
|
||||||
agent, err := h.agentQueries.GetAgentByID(req.AgentID)
|
agent, err := h.agentQueries.GetAgentByID(agentIDUUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "agent not found"})
|
c.JSON(http.StatusNotFound, gin.H{"error": "agent not found"})
|
||||||
return
|
return
|
||||||
@@ -77,12 +113,70 @@ func (h *AgentUpdateHandler) UpdateAgent(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update agent status to "updating"
|
// Update agent status to "updating"
|
||||||
if err := h.agentQueries.UpdateAgentUpdatingStatus(req.AgentID, true, &req.Version); err != nil {
|
if err := h.agentQueries.UpdateAgentUpdatingStatus(agentIDUUID, true, &req.Version); err != nil {
|
||||||
log.Printf("Failed to update agent %s status to updating: %v", req.AgentID, err)
|
log.Printf("Failed to update agent %s status to updating: %v", agentID, err)
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to initiate update"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to initiate update"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate the provided nonce
|
||||||
|
if h.nonceService != nil {
|
||||||
|
if debugMode {
|
||||||
|
log.Printf("[DEBUG] [UpdateAgent] Validating nonce for agent %s: %s", agentID, req.Nonce)
|
||||||
|
}
|
||||||
|
verifiedNonce, err := h.nonceService.Validate(req.Nonce)
|
||||||
|
if err != nil {
|
||||||
|
h.agentQueries.UpdateAgentUpdatingStatus(agentIDUUID, false, nil) // Rollback
|
||||||
|
log.Printf("[UPDATE] Nonce validation failed for agent %s: %v", agentID, err)
|
||||||
|
// Include specific error context for debugging
|
||||||
|
errorType := "signature_verification_failed"
|
||||||
|
if err.Error() == "nonce expired" {
|
||||||
|
errorType = "nonce_expired"
|
||||||
|
} else if err.Error() == "invalid base64" {
|
||||||
|
errorType = "invalid_nonce_format"
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"error": "invalid update nonce: " + err.Error(),
|
||||||
|
"_error_context": errorType,
|
||||||
|
"_error_detail": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if debugMode {
|
||||||
|
log.Printf("[DEBUG] [UpdateAgent] Nonce verified - AgentID: %s, TargetVersion: %s", verifiedNonce.AgentID, verifiedNonce.TargetVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the nonce matches the requested agent and version
|
||||||
|
if verifiedNonce.AgentID != agentID {
|
||||||
|
if debugMode {
|
||||||
|
log.Printf("[DEBUG] [UpdateAgent] Agent ID mismatch - nonce: %s, URL: %s", verifiedNonce.AgentID, agentID)
|
||||||
|
}
|
||||||
|
log.Printf("[UPDATE] Agent ID mismatch in nonce: expected %s, got %s", agentID, verifiedNonce.AgentID)
|
||||||
|
h.agentQueries.UpdateAgentUpdatingStatus(agentIDUUID, false, nil) // Rollback
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"error": "nonce agent ID mismatch",
|
||||||
|
"_agent_id": agentID,
|
||||||
|
"_nonce_agent_id": verifiedNonce.AgentID,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if verifiedNonce.TargetVersion != req.Version {
|
||||||
|
if debugMode {
|
||||||
|
log.Printf("[DEBUG] [UpdateAgent] Version mismatch - nonce: %s, request: %s", verifiedNonce.TargetVersion, req.Version)
|
||||||
|
}
|
||||||
|
log.Printf("[UPDATE] Version mismatch in nonce: expected %s, got %s", req.Version, verifiedNonce.TargetVersion)
|
||||||
|
h.agentQueries.UpdateAgentUpdatingStatus(agentIDUUID, false, nil) // Rollback
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"error": "nonce version mismatch",
|
||||||
|
"_requested_version": req.Version,
|
||||||
|
"_nonce_version": verifiedNonce.TargetVersion,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("[UPDATE] Nonce successfully validated for agent %s to version %s", agentID, req.Version)
|
||||||
|
}
|
||||||
|
|
||||||
// Generate nonce for replay protection
|
// Generate nonce for replay protection
|
||||||
nonceUUID := uuid.New()
|
nonceUUID := uuid.New()
|
||||||
nonceTimestamp := time.Now()
|
nonceTimestamp := time.Now()
|
||||||
@@ -398,4 +492,189 @@ func (h *AgentUpdateHandler) estimateUpdateTime(fileSize int64) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return seconds
|
return seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateUpdateNonce handles POST /api/v1/agents/:id/update-nonce
|
||||||
|
func (h *AgentUpdateHandler) GenerateUpdateNonce(c *gin.Context) {
|
||||||
|
agentID := c.Param("id")
|
||||||
|
targetVersion := c.Query("target_version")
|
||||||
|
|
||||||
|
if targetVersion == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "target_version query parameter required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.nonceService == nil {
|
||||||
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "nonce service not available"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse agent ID as UUID
|
||||||
|
agentIDUUID, err := uuid.Parse(agentID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID format"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify agent exists
|
||||||
|
agent, err := h.agentQueries.GetAgentByID(agentIDUUID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "agent not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate nonce
|
||||||
|
nonce, err := h.nonceService.Generate(agentID, targetVersion)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] Failed to generate update nonce for agent %s: %v", agentID, err)
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate nonce"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[system] Generated update nonce for agent %s (%s) -> %s", agentID, agent.Hostname, targetVersion)
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"agent_id": agentID,
|
||||||
|
"hostname": agent.Hostname,
|
||||||
|
"current_version": agent.CurrentVersion,
|
||||||
|
"target_version": targetVersion,
|
||||||
|
"update_nonce": nonce,
|
||||||
|
"expires_at": time.Now().Add(10 * time.Minute).Unix(),
|
||||||
|
"expires_in_seconds": 600,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckForUpdateAvailable handles GET /api/v1/agents/:id/updates/available
|
||||||
|
func (h *AgentUpdateHandler) CheckForUpdateAvailable(c *gin.Context) {
|
||||||
|
agentID := c.Param("id")
|
||||||
|
|
||||||
|
// Parse agent ID
|
||||||
|
agentIDUUID, err := uuid.Parse(agentID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID format"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query database for agent's current version
|
||||||
|
agent, err := h.agentQueries.GetAgentByID(agentIDUUID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "agent not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Platform format: separate os_type and os_architecture from agent data
|
||||||
|
osType := strings.ToLower(agent.OSType)
|
||||||
|
osArch := agent.OSArchitecture
|
||||||
|
|
||||||
|
// Check if newer version available from agent_update_packages table
|
||||||
|
latestVersion, err := h.agentUpdateQueries.GetLatestVersionByTypeAndArch(osType, osArch)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[DEBUG] GetLatestVersionByTypeAndArch error for %s/%s: %v", osType, osArch, err)
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"hasUpdate": false,
|
||||||
|
"reason": "no packages available",
|
||||||
|
"currentVersion": agent.CurrentVersion,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is actually newer than current version
|
||||||
|
hasUpdate := isVersionUpgrade(latestVersion, agent.CurrentVersion)
|
||||||
|
|
||||||
|
log.Printf("[DEBUG] Version comparison - latest: %s, current: %s, hasUpdate: %v for platform: %s/%s", latestVersion, agent.CurrentVersion, hasUpdate, osType, osArch)
|
||||||
|
|
||||||
|
// Special handling for sub-versions (0.1.23.5 vs 0.1.23)
|
||||||
|
if !hasUpdate && strings.HasPrefix(latestVersion, agent.CurrentVersion + ".") {
|
||||||
|
hasUpdate = true
|
||||||
|
log.Printf("[DEBUG] Detected sub-version upgrade: %s -> %s", agent.CurrentVersion, latestVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"hasUpdate": hasUpdate,
|
||||||
|
"currentVersion": agent.CurrentVersion,
|
||||||
|
"latestVersion": latestVersion,
|
||||||
|
"platform": osType + "-" + osArch,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUpdateStatus handles GET /api/v1/agents/:id/updates/status
|
||||||
|
func (h *AgentUpdateHandler) GetUpdateStatus(c *gin.Context) {
|
||||||
|
agentID := c.Param("id")
|
||||||
|
|
||||||
|
// Parse agent ID
|
||||||
|
agentIDUUID, err := uuid.Parse(agentID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID format"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch agent with update state
|
||||||
|
agent, err := h.agentQueries.GetAgentByID(agentIDUUID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "agent not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine status from agent state + recent commands
|
||||||
|
var status string
|
||||||
|
var progress *int
|
||||||
|
var errorMsg *string
|
||||||
|
|
||||||
|
if agent.IsUpdating {
|
||||||
|
// Check if agent has pending update command
|
||||||
|
cmd, err := h.agentUpdateQueries.GetPendingUpdateCommand(agentID)
|
||||||
|
if err == nil && cmd != nil {
|
||||||
|
status = "downloading"
|
||||||
|
// Progress could be based on last acknowledgment time
|
||||||
|
if time.Since(cmd.CreatedAt) > 2*time.Minute {
|
||||||
|
status = "installing"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
status = "pending"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
status = "idle"
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"status": status,
|
||||||
|
"progress": progress,
|
||||||
|
"error": errorMsg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// isVersionUpgrade returns true if new version is greater than current version
|
||||||
|
func isVersionUpgrade(newVersion string, currentVersion string) bool {
|
||||||
|
// Parse semantic versions
|
||||||
|
newParts := strings.Split(newVersion, ".")
|
||||||
|
curParts := strings.Split(currentVersion, ".")
|
||||||
|
|
||||||
|
// Pad arrays to 3 parts
|
||||||
|
for len(newParts) < 3 {
|
||||||
|
newParts = append(newParts, "0")
|
||||||
|
}
|
||||||
|
for len(curParts) < 3 {
|
||||||
|
curParts = append(curParts, "0")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to integers for comparison
|
||||||
|
newMajor, _ := strconv.Atoi(newParts[0])
|
||||||
|
newMinor, _ := strconv.Atoi(newParts[1])
|
||||||
|
newPatch, _ := strconv.Atoi(newParts[2])
|
||||||
|
|
||||||
|
curMajor, _ := strconv.Atoi(curParts[0])
|
||||||
|
curMinor, _ := strconv.Atoi(curParts[1])
|
||||||
|
curPatch, _ := strconv.Atoi(curParts[2])
|
||||||
|
|
||||||
|
// Check if new > current (not equal, not less)
|
||||||
|
if newMajor > curMajor {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if newMajor == curMajor && newMinor > curMinor {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if newMajor == curMajor && newMinor == curMinor && newPatch > curPatch {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
@@ -31,24 +31,28 @@ func (h *DownloadHandler) getServerURL(c *gin.Context) string {
|
|||||||
return h.config.Server.PublicURL
|
return h.config.Server.PublicURL
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority 2: Construct API server URL from configuration
|
// Priority 2: Detect from request with TLS/proxy awareness
|
||||||
scheme := "http"
|
scheme := "http"
|
||||||
host := h.config.Server.Host
|
|
||||||
port := h.config.Server.Port
|
|
||||||
|
|
||||||
// Use HTTPS if TLS is enabled in config
|
// Check if TLS is enabled in config
|
||||||
if h.config.Server.TLS.Enabled {
|
if h.config.Server.TLS.Enabled {
|
||||||
scheme = "https"
|
scheme = "https"
|
||||||
}
|
}
|
||||||
|
|
||||||
// For default host (0.0.0.0), use localhost for client connections
|
// Check if request came through HTTPS (direct or via proxy)
|
||||||
if host == "0.0.0.0" {
|
if c.Request.TLS != nil {
|
||||||
host = "localhost"
|
scheme = "https"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only include port if it's not the default for the protocol
|
// Check X-Forwarded-Proto for reverse proxy setups
|
||||||
if (scheme == "http" && port != 80) || (scheme == "https" && port != 443) {
|
if forwardedProto := c.GetHeader("X-Forwarded-Proto"); forwardedProto == "https" {
|
||||||
return fmt.Sprintf("%s://%s:%d", scheme, host, port)
|
scheme = "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the Host header exactly as received (includes port if present)
|
||||||
|
host := c.GetHeader("X-Forwarded-Host")
|
||||||
|
if host == "" {
|
||||||
|
host = c.Request.Host
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("%s://%s", scheme, host)
|
return fmt.Sprintf("%s://%s", scheme, host)
|
||||||
@@ -151,7 +155,6 @@ AGENT_BINARY="/usr/local/bin/redflag-agent"
|
|||||||
SUDOERS_FILE="/etc/sudoers.d/redflag-agent"
|
SUDOERS_FILE="/etc/sudoers.d/redflag-agent"
|
||||||
SERVICE_FILE="/etc/systemd/system/redflag-agent.service"
|
SERVICE_FILE="/etc/systemd/system/redflag-agent.service"
|
||||||
CONFIG_DIR="/etc/aggregator"
|
CONFIG_DIR="/etc/aggregator"
|
||||||
STATE_DIR="/var/lib/aggregator"
|
|
||||||
|
|
||||||
echo "=== RedFlag Agent Installation ==="
|
echo "=== RedFlag Agent Installation ==="
|
||||||
echo ""
|
echo ""
|
||||||
@@ -298,24 +301,19 @@ else
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Step 4: Create configuration and state directories
|
# Step 4: Create configuration directory
|
||||||
echo ""
|
echo ""
|
||||||
echo "Step 4: Creating configuration and state directories..."
|
echo "Step 4: Creating configuration directory..."
|
||||||
mkdir -p "$CONFIG_DIR"
|
mkdir -p "$CONFIG_DIR"
|
||||||
chown "$AGENT_USER:$AGENT_USER" "$CONFIG_DIR"
|
chown "$AGENT_USER:$AGENT_USER" "$CONFIG_DIR"
|
||||||
chmod 755 "$CONFIG_DIR"
|
chmod 755 "$CONFIG_DIR"
|
||||||
|
echo "✓ Configuration directory created"
|
||||||
|
|
||||||
# Create state directory for acknowledgment tracking (v0.1.19+)
|
# Set SELinux context for config directory if SELinux is enabled
|
||||||
mkdir -p "$STATE_DIR"
|
|
||||||
chown "$AGENT_USER:$AGENT_USER" "$STATE_DIR"
|
|
||||||
chmod 755 "$STATE_DIR"
|
|
||||||
echo "✓ Configuration and state directories created"
|
|
||||||
|
|
||||||
# Set SELinux context for directories if SELinux is enabled
|
|
||||||
if command -v getenforce >/dev/null 2>&1 && [ "$(getenforce)" != "Disabled" ]; then
|
if command -v getenforce >/dev/null 2>&1 && [ "$(getenforce)" != "Disabled" ]; then
|
||||||
echo "Setting SELinux context for directories..."
|
echo "Setting SELinux context for config directory..."
|
||||||
restorecon -Rv "$CONFIG_DIR" "$STATE_DIR" 2>/dev/null || true
|
restorecon -Rv "$CONFIG_DIR" 2>/dev/null || true
|
||||||
echo "✓ SELinux context set for directories"
|
echo "✓ SELinux context set for config directory"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Step 5: Install systemd service
|
# Step 5: Install systemd service
|
||||||
@@ -340,7 +338,7 @@ RestartSec=30
|
|||||||
# NoNewPrivileges=true - DISABLED: Prevents sudo from working, which agent needs for package management
|
# NoNewPrivileges=true - DISABLED: Prevents sudo from working, which agent needs for package management
|
||||||
ProtectSystem=strict
|
ProtectSystem=strict
|
||||||
ProtectHome=true
|
ProtectHome=true
|
||||||
ReadWritePaths=$AGENT_HOME /var/log $CONFIG_DIR $STATE_DIR
|
ReadWritePaths=$AGENT_HOME /var/log $CONFIG_DIR
|
||||||
PrivateTmp=true
|
PrivateTmp=true
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ type Config struct {
|
|||||||
LatestAgentVersion string
|
LatestAgentVersion string
|
||||||
MinAgentVersion string `env:"MIN_AGENT_VERSION" default:"0.1.22"`
|
MinAgentVersion string `env:"MIN_AGENT_VERSION" default:"0.1.22"`
|
||||||
SigningPrivateKey string `env:"REDFLAG_SIGNING_PRIVATE_KEY"`
|
SigningPrivateKey string `env:"REDFLAG_SIGNING_PRIVATE_KEY"`
|
||||||
|
DebugEnabled bool `env:"REDFLAG_DEBUG" default:"false"` // Enable debug logging
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load reads configuration from environment variables only (immutable configuration)
|
// Load reads configuration from environment variables only (immutable configuration)
|
||||||
@@ -86,7 +87,7 @@ func Load() (*Config, error) {
|
|||||||
cfg.CheckInInterval = checkInInterval
|
cfg.CheckInInterval = checkInInterval
|
||||||
cfg.OfflineThreshold = offlineThreshold
|
cfg.OfflineThreshold = offlineThreshold
|
||||||
cfg.Timezone = getEnv("TIMEZONE", "UTC")
|
cfg.Timezone = getEnv("TIMEZONE", "UTC")
|
||||||
cfg.LatestAgentVersion = getEnv("LATEST_AGENT_VERSION", "0.1.23")
|
cfg.LatestAgentVersion = getEnv("LATEST_AGENT_VERSION", "0.1.23.5")
|
||||||
cfg.MinAgentVersion = getEnv("MIN_AGENT_VERSION", "0.1.22")
|
cfg.MinAgentVersion = getEnv("MIN_AGENT_VERSION", "0.1.22")
|
||||||
cfg.SigningPrivateKey = getEnv("REDFLAG_SIGNING_PRIVATE_KEY", "")
|
cfg.SigningPrivateKey = getEnv("REDFLAG_SIGNING_PRIVATE_KEY", "")
|
||||||
|
|
||||||
|
|||||||
@@ -216,4 +216,66 @@ func (q *AgentUpdateQueries) GetAgentByMachineID(machineID string) (*models.Agen
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &agent, nil
|
return &agent, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLatestVersion retrieves the latest available version for a platform
|
||||||
|
func (q *AgentUpdateQueries) GetLatestVersion(platform string) (string, error) {
|
||||||
|
query := `
|
||||||
|
SELECT version FROM agent_update_packages
|
||||||
|
WHERE platform = $1 AND is_active = true
|
||||||
|
ORDER BY version DESC LIMIT 1
|
||||||
|
`
|
||||||
|
|
||||||
|
var latestVersion string
|
||||||
|
err := q.db.Get(&latestVersion, query, platform)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return "", fmt.Errorf("no update packages available for platform %s", platform)
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("failed to get latest version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return latestVersion, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLatestVersionByTypeAndArch retrieves the latest available version for a specific os_type and architecture
|
||||||
|
func (q *AgentUpdateQueries) GetLatestVersionByTypeAndArch(osType, osArch string) (string, error) {
|
||||||
|
query := `
|
||||||
|
SELECT version FROM agent_update_packages
|
||||||
|
WHERE platform = $1 AND architecture = $2 AND is_active = true
|
||||||
|
ORDER BY version DESC LIMIT 1
|
||||||
|
`
|
||||||
|
|
||||||
|
var latestVersion string
|
||||||
|
err := q.db.Get(&latestVersion, query, osType, osArch)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return "", fmt.Errorf("no update packages available for platform %s/%s", osType, osArch)
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("failed to get latest version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return latestVersion, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPendingUpdateCommand retrieves the most recent pending update command for an agent
|
||||||
|
func (q *AgentUpdateQueries) GetPendingUpdateCommand(agentID string) (*models.AgentCommand, error) {
|
||||||
|
query := `
|
||||||
|
SELECT id, agent_id, command_type, params, status, source, created_at, sent_at, completed_at, result, retried_from_id
|
||||||
|
FROM agent_commands
|
||||||
|
WHERE agent_id = $1 AND command_type = 'install_update' AND status = 'pending'
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
`
|
||||||
|
|
||||||
|
var command models.AgentCommand
|
||||||
|
err := q.db.Get(&command, query, agentID)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil // No pending update command found
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to get pending update command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &command, nil
|
||||||
}
|
}
|
||||||
@@ -23,10 +23,11 @@ type AgentUpdatePackage struct {
|
|||||||
|
|
||||||
// AgentUpdateRequest represents a request to update an agent
|
// AgentUpdateRequest represents a request to update an agent
|
||||||
type AgentUpdateRequest struct {
|
type AgentUpdateRequest struct {
|
||||||
AgentID uuid.UUID `json:"agent_id" binding:"required"`
|
AgentID uuid.UUID `json:"agent_id,omitempty"` // Optional when agent ID is in URL path
|
||||||
Version string `json:"version" binding:"required"`
|
Version string `json:"version" binding:"required"`
|
||||||
Platform string `json:"platform" binding:"required"`
|
Platform string `json:"platform" binding:"required"`
|
||||||
Scheduled *string `json:"scheduled_at,omitempty"`
|
Scheduled *string `json:"scheduled_at,omitempty"`
|
||||||
|
Nonce string `json:"nonce" binding:"required"` // Required security nonce to prevent replay attacks
|
||||||
}
|
}
|
||||||
|
|
||||||
// BulkAgentUpdateRequest represents a bulk update request
|
// BulkAgentUpdateRequest represents a bulk update request
|
||||||
|
|||||||
@@ -73,6 +73,23 @@ export function AgentUpdatesModal({
|
|||||||
const pkg = packages.find(p => p.id === packageId);
|
const pkg = packages.find(p => p.id === packageId);
|
||||||
if (!pkg) throw new Error('Package not found');
|
if (!pkg) throw new Error('Package not found');
|
||||||
|
|
||||||
|
// For single agent updates, use individual update with nonce for security
|
||||||
|
if (selectedAgentIds.length === 1) {
|
||||||
|
const agentId = selectedAgentIds[0];
|
||||||
|
|
||||||
|
// Generate nonce for security
|
||||||
|
const nonceData = await agentApi.generateUpdateNonce(agentId, pkg.version);
|
||||||
|
console.log('[UI] Update nonce generated for single agent:', nonceData);
|
||||||
|
|
||||||
|
// Use individual update endpoint with nonce
|
||||||
|
return agentApi.updateAgent(agentId, {
|
||||||
|
version: pkg.version,
|
||||||
|
platform: pkg.platform,
|
||||||
|
nonce: nonceData.update_nonce
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// For multiple agents, use bulk update
|
||||||
const updateData = {
|
const updateData = {
|
||||||
agent_ids: selectedAgentIds,
|
agent_ids: selectedAgentIds,
|
||||||
version: pkg.version,
|
version: pkg.version,
|
||||||
@@ -82,7 +99,9 @@ export function AgentUpdatesModal({
|
|||||||
return agentApi.updateMultipleAgents(updateData);
|
return agentApi.updateMultipleAgents(updateData);
|
||||||
},
|
},
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
toast.success(`Update initiated for ${data.updated?.length || 0} agent(s)`);
|
const count = selectedAgentIds.length;
|
||||||
|
const message = count === 1 ? 'Update initiated for agent' : `Update initiated for ${count} agents`;
|
||||||
|
toast.success(message);
|
||||||
setIsUpdating(false);
|
setIsUpdating(false);
|
||||||
onAgentsUpdated();
|
onAgentsUpdated();
|
||||||
onClose();
|
onClose();
|
||||||
|
|||||||
@@ -172,6 +172,24 @@ export const agentApi = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Check if update available for agent
|
||||||
|
checkForUpdateAvailable: async (agentId: string): Promise<{ hasUpdate: boolean; currentVersion: string; latestVersion?: string; reason?: string; platform?: string }> => {
|
||||||
|
const response = await api.get(`/agents/${agentId}/updates/available`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get update status for agent
|
||||||
|
getUpdateStatus: async (agentId: string): Promise<{ status: string; progress?: number; error?: string }> => {
|
||||||
|
const response = await api.get(`/agents/${agentId}/updates/status`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Generate update nonce for agent (new security feature)
|
||||||
|
generateUpdateNonce: async (agentId: string, targetVersion: string): Promise<{ agent_id: string; target_version: string; update_nonce: string; expires_at: number; expires_in_seconds: number }> => {
|
||||||
|
const response = await api.post(`/agents/${agentId}/update-nonce?target_version=${targetVersion}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
// Update multiple agents (bulk)
|
// Update multiple agents (bulk)
|
||||||
updateMultipleAgents: async (updateData: {
|
updateMultipleAgents: async (updateData: {
|
||||||
agent_ids: string[];
|
agent_ids: string[];
|
||||||
|
|||||||
Reference in New Issue
Block a user