v0.1.16: Security overhaul and systematic deployment preparation
Breaking changes for clean alpha releases: - JWT authentication with user-provided secrets (no more development defaults) - Registration token system for secure agent enrollment - Rate limiting with user-adjustable settings - Enhanced agent configuration with proxy support - Interactive server setup wizard (--setup flag) - Heartbeat architecture separation for better UX - Package status synchronization fixes - Accurate timestamp tracking for RMM features Setup process for new installations: 1. docker-compose up -d postgres 2. ./redflag-server --setup 3. ./redflag-server --migrate 4. ./redflag-server 5. Generate tokens via admin UI 6. Deploy agents with registration tokens
This commit is contained in:
BIN
aggregator-agent/aggregator-agent
Executable file
BIN
aggregator-agent/aggregator-agent
Executable file
Binary file not shown.
@@ -21,7 +21,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
AgentVersion = "0.1.8" // Added dnf makecache to security allowlist, retry tracking
|
||||
AgentVersion = "0.1.16" // Enhanced configuration system with proxy support and registration tokens
|
||||
)
|
||||
|
||||
// getConfigPath returns the platform-specific config path
|
||||
@@ -32,6 +32,26 @@ func getConfigPath() string {
|
||||
return "/etc/aggregator/config.json"
|
||||
}
|
||||
|
||||
// getCurrentPollingInterval returns the appropriate polling interval based on rapid mode
|
||||
func getCurrentPollingInterval(cfg *config.Config) int {
|
||||
// Check if rapid polling mode is active and not expired
|
||||
if cfg.RapidPollingEnabled && time.Now().Before(cfg.RapidPollingUntil) {
|
||||
return 5 // Rapid polling: 5 seconds
|
||||
}
|
||||
|
||||
// Check if rapid polling has expired and clean up
|
||||
if cfg.RapidPollingEnabled && time.Now().After(cfg.RapidPollingUntil) {
|
||||
cfg.RapidPollingEnabled = false
|
||||
cfg.RapidPollingUntil = time.Time{}
|
||||
// Save the updated config to clean up expired rapid mode
|
||||
if err := cfg.Save(getConfigPath()); err != nil {
|
||||
log.Printf("Warning: Failed to cleanup expired rapid polling mode: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return cfg.CheckInInterval // Normal polling: 5 minutes (300 seconds) by default
|
||||
}
|
||||
|
||||
// getDefaultServerURL returns the default server URL with environment variable support
|
||||
func getDefaultServerURL() string {
|
||||
// Check environment variable first
|
||||
@@ -48,16 +68,65 @@ func getDefaultServerURL() string {
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Define CLI flags
|
||||
registerCmd := flag.Bool("register", false, "Register agent with server")
|
||||
scanCmd := flag.Bool("scan", false, "Scan for updates and display locally")
|
||||
statusCmd := flag.Bool("status", false, "Show agent status")
|
||||
listUpdatesCmd := flag.Bool("list-updates", false, "List detailed update information")
|
||||
serverURL := flag.String("server", getDefaultServerURL(), "Server URL")
|
||||
versionCmd := flag.Bool("version", false, "Show version information")
|
||||
serverURL := flag.String("server", "", "Server URL")
|
||||
registrationToken := flag.String("token", "", "Registration token for secure enrollment")
|
||||
proxyHTTP := flag.String("proxy-http", "", "HTTP proxy URL")
|
||||
proxyHTTPS := flag.String("proxy-https", "", "HTTPS proxy URL")
|
||||
proxyNoProxy := flag.String("proxy-no", "", "Comma-separated hosts to bypass proxy")
|
||||
logLevel := flag.String("log-level", "", "Log level (debug, info, warn, error)")
|
||||
configFile := flag.String("config", "", "Configuration file path")
|
||||
tagsFlag := flag.String("tags", "", "Comma-separated tags for agent")
|
||||
organization := flag.String("organization", "", "Organization/group name")
|
||||
displayName := flag.String("name", "", "Display name for agent")
|
||||
insecureTLS := flag.Bool("insecure-tls", false, "Skip TLS certificate verification")
|
||||
exportFormat := flag.String("export", "", "Export format: json, csv")
|
||||
flag.Parse()
|
||||
|
||||
// Load configuration
|
||||
cfg, err := config.Load(getConfigPath())
|
||||
// Handle version command
|
||||
if *versionCmd {
|
||||
fmt.Printf("RedFlag Agent v%s\n", AgentVersion)
|
||||
fmt.Printf("Self-hosted update management platform\n")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Parse tags from comma-separated string
|
||||
var tags []string
|
||||
if *tagsFlag != "" {
|
||||
tags = strings.Split(*tagsFlag, ",")
|
||||
for i, tag := range tags {
|
||||
tags[i] = strings.TrimSpace(tag)
|
||||
}
|
||||
}
|
||||
|
||||
// Create CLI flags structure
|
||||
cliFlags := &config.CLIFlags{
|
||||
ServerURL: *serverURL,
|
||||
RegistrationToken: *registrationToken,
|
||||
ProxyHTTP: *proxyHTTP,
|
||||
ProxyHTTPS: *proxyHTTPS,
|
||||
ProxyNoProxy: *proxyNoProxy,
|
||||
LogLevel: *logLevel,
|
||||
ConfigFile: *configFile,
|
||||
Tags: tags,
|
||||
Organization: *organization,
|
||||
DisplayName: *displayName,
|
||||
InsecureTLS: *insecureTLS,
|
||||
}
|
||||
|
||||
// Determine config path
|
||||
configPath := getConfigPath()
|
||||
if *configFile != "" {
|
||||
configPath = *configFile
|
||||
}
|
||||
|
||||
// Load configuration with priority: CLI > env > file > defaults
|
||||
cfg, err := config.Load(configPath, cliFlags)
|
||||
if err != nil {
|
||||
log.Fatal("Failed to load configuration:", err)
|
||||
}
|
||||
@@ -313,6 +382,24 @@ func runAgent(cfg *config.Config) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Add heartbeat status to metrics metadata if available
|
||||
if metrics != nil && cfg.RapidPollingEnabled {
|
||||
// Check if rapid polling is still valid
|
||||
if time.Now().Before(cfg.RapidPollingUntil) {
|
||||
// Include heartbeat metadata in metrics
|
||||
if metrics.Metadata == nil {
|
||||
metrics.Metadata = make(map[string]interface{})
|
||||
}
|
||||
metrics.Metadata["rapid_polling_enabled"] = true
|
||||
metrics.Metadata["rapid_polling_until"] = cfg.RapidPollingUntil.Format(time.RFC3339)
|
||||
metrics.Metadata["rapid_polling_duration_minutes"] = int(time.Until(cfg.RapidPollingUntil).Minutes())
|
||||
} else {
|
||||
// Heartbeat expired, disable it
|
||||
cfg.RapidPollingEnabled = false
|
||||
cfg.RapidPollingUntil = time.Time{}
|
||||
}
|
||||
}
|
||||
|
||||
// Get commands from server (with optional metrics)
|
||||
commands, err := apiClient.GetCommands(cfg.AgentID, metrics)
|
||||
if err != nil {
|
||||
@@ -320,7 +407,7 @@ func runAgent(cfg *config.Config) error {
|
||||
newClient, renewErr := renewTokenIfNeeded(apiClient, cfg, err)
|
||||
if renewErr != nil {
|
||||
log.Printf("Check-in unsuccessful and token renewal failed: %v\n", renewErr)
|
||||
time.Sleep(time.Duration(cfg.CheckInInterval) * time.Second)
|
||||
time.Sleep(time.Duration(getCurrentPollingInterval(cfg)) * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -331,12 +418,12 @@ func runAgent(cfg *config.Config) error {
|
||||
commands, err = apiClient.GetCommands(cfg.AgentID, metrics)
|
||||
if err != nil {
|
||||
log.Printf("Check-in unsuccessful even after token renewal: %v\n", err)
|
||||
time.Sleep(time.Duration(cfg.CheckInInterval) * time.Second)
|
||||
time.Sleep(time.Duration(getCurrentPollingInterval(cfg)) * time.Second)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
log.Printf("Check-in unsuccessful: %v\n", err)
|
||||
time.Sleep(time.Duration(cfg.CheckInInterval) * time.Second)
|
||||
time.Sleep(time.Duration(getCurrentPollingInterval(cfg)) * time.Second)
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -375,13 +462,23 @@ func runAgent(cfg *config.Config) error {
|
||||
log.Printf("Error confirming dependencies: %v\n", err)
|
||||
}
|
||||
|
||||
case "enable_heartbeat":
|
||||
if err := handleEnableHeartbeat(apiClient, cfg, cmd.ID, cmd.Params); err != nil {
|
||||
log.Printf("[Heartbeat] Error enabling heartbeat: %v\n", err)
|
||||
}
|
||||
|
||||
case "disable_heartbeat":
|
||||
if err := handleDisableHeartbeat(apiClient, cfg, cmd.ID); err != nil {
|
||||
log.Printf("[Heartbeat] Error disabling heartbeat: %v\n", err)
|
||||
}
|
||||
|
||||
default:
|
||||
log.Printf("Unknown command type: %s\n", cmd.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for next check-in
|
||||
time.Sleep(time.Duration(cfg.CheckInInterval) * time.Second)
|
||||
time.Sleep(time.Duration(getCurrentPollingInterval(cfg)) * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -743,9 +840,9 @@ func handleInstallUpdates(apiClient *client.Client, cfg *config.Config, commandI
|
||||
|
||||
// Perform installation based on what's specified
|
||||
if packageName != "" {
|
||||
action = "install"
|
||||
log.Printf("Installing package: %s (type: %s)", packageName, packageType)
|
||||
result, err = inst.Install(packageName)
|
||||
action = "update"
|
||||
log.Printf("Updating package: %s (type: %s)", packageName, packageType)
|
||||
result, err = inst.UpdatePackage(packageName)
|
||||
} else if len(params) > 1 {
|
||||
// Multiple packages might be specified in various ways
|
||||
var packageNames []string
|
||||
@@ -774,15 +871,15 @@ func handleInstallUpdates(apiClient *client.Client, cfg *config.Config, commandI
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// Report installation failure
|
||||
// Report installation failure with actual command output
|
||||
logReport := client.LogReport{
|
||||
CommandID: commandID,
|
||||
Action: action,
|
||||
Result: "failed",
|
||||
Stdout: "",
|
||||
Stderr: fmt.Sprintf("Installation error: %v", err),
|
||||
ExitCode: 1,
|
||||
DurationSeconds: 0,
|
||||
Stdout: result.Stdout,
|
||||
Stderr: result.Stderr,
|
||||
ExitCode: result.ExitCode,
|
||||
DurationSeconds: result.DurationSeconds,
|
||||
}
|
||||
|
||||
if reportErr := apiClient.ReportLog(cfg.AgentID, logReport); reportErr != nil {
|
||||
@@ -991,21 +1088,22 @@ func handleConfirmDependencies(apiClient *client.Client, cfg *config.Config, com
|
||||
allPackages := append([]string{packageName}, dependencies...)
|
||||
result, err = inst.InstallMultiple(allPackages)
|
||||
} else {
|
||||
action = "install"
|
||||
action = "upgrade"
|
||||
log.Printf("Installing package: %s (no dependencies)", packageName)
|
||||
result, err = inst.Install(packageName)
|
||||
// Use UpdatePackage instead of Install to handle existing packages
|
||||
result, err = inst.UpdatePackage(packageName)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// Report installation failure
|
||||
// Report installation failure with actual command output
|
||||
logReport := client.LogReport{
|
||||
CommandID: commandID,
|
||||
Action: action,
|
||||
Result: "failed",
|
||||
Stdout: "",
|
||||
Stderr: fmt.Sprintf("Installation error: %v", err),
|
||||
ExitCode: 1,
|
||||
DurationSeconds: 0,
|
||||
Stdout: result.Stdout,
|
||||
Stderr: result.Stderr,
|
||||
ExitCode: result.ExitCode,
|
||||
DurationSeconds: result.DurationSeconds,
|
||||
}
|
||||
|
||||
if reportErr := apiClient.ReportLog(cfg.AgentID, logReport); reportErr != nil {
|
||||
@@ -1051,6 +1149,145 @@ func handleConfirmDependencies(apiClient *client.Client, cfg *config.Config, com
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleEnableHeartbeat handles enable_heartbeat command
|
||||
func handleEnableHeartbeat(apiClient *client.Client, cfg *config.Config, commandID string, params map[string]interface{}) error {
|
||||
// Parse duration parameter (default to 10 minutes)
|
||||
durationMinutes := 10
|
||||
if duration, ok := params["duration_minutes"]; ok {
|
||||
if durationFloat, ok := duration.(float64); ok {
|
||||
durationMinutes = int(durationFloat)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate when heartbeat should expire
|
||||
expiryTime := time.Now().Add(time.Duration(durationMinutes) * time.Minute)
|
||||
|
||||
log.Printf("[Heartbeat] Enabling rapid polling for %d minutes (expires: %s)", durationMinutes, expiryTime.Format(time.RFC3339))
|
||||
|
||||
// Update agent config to enable rapid polling
|
||||
cfg.RapidPollingEnabled = true
|
||||
cfg.RapidPollingUntil = expiryTime
|
||||
|
||||
// Save config to persist heartbeat settings
|
||||
if err := cfg.Save(getConfigPath()); err != nil {
|
||||
log.Printf("[Heartbeat] Warning: Failed to save config: %v", err)
|
||||
}
|
||||
|
||||
// Create log report for heartbeat enable
|
||||
logReport := client.LogReport{
|
||||
CommandID: commandID,
|
||||
Action: "enable_heartbeat",
|
||||
Result: "success",
|
||||
Stdout: fmt.Sprintf("Heartbeat enabled for %d minutes", durationMinutes),
|
||||
Stderr: "",
|
||||
ExitCode: 0,
|
||||
DurationSeconds: 0,
|
||||
}
|
||||
|
||||
if reportErr := apiClient.ReportLog(cfg.AgentID, logReport); reportErr != nil {
|
||||
log.Printf("[Heartbeat] Failed to report heartbeat enable: %v", reportErr)
|
||||
}
|
||||
|
||||
// Send immediate check-in to update heartbeat status in UI
|
||||
log.Printf("[Heartbeat] Sending immediate check-in to update status")
|
||||
sysMetrics, err := system.GetLightweightMetrics()
|
||||
if err == nil {
|
||||
metrics := &client.SystemMetrics{
|
||||
CPUPercent: sysMetrics.CPUPercent,
|
||||
MemoryPercent: sysMetrics.MemoryPercent,
|
||||
MemoryUsedGB: sysMetrics.MemoryUsedGB,
|
||||
MemoryTotalGB: sysMetrics.MemoryTotalGB,
|
||||
DiskUsedGB: sysMetrics.DiskUsedGB,
|
||||
DiskTotalGB: sysMetrics.DiskTotalGB,
|
||||
DiskPercent: sysMetrics.DiskPercent,
|
||||
Uptime: sysMetrics.Uptime,
|
||||
Version: AgentVersion,
|
||||
}
|
||||
// Include heartbeat metadata to show enabled state
|
||||
metrics.Metadata = map[string]interface{}{
|
||||
"rapid_polling_enabled": true,
|
||||
"rapid_polling_until": expiryTime.Format(time.RFC3339),
|
||||
}
|
||||
|
||||
// Send immediate check-in with updated heartbeat status
|
||||
_, checkinErr := apiClient.GetCommands(cfg.AgentID, metrics)
|
||||
if checkinErr != nil {
|
||||
log.Printf("[Heartbeat] Failed to send immediate check-in: %v", checkinErr)
|
||||
} else {
|
||||
log.Printf("[Heartbeat] Immediate check-in sent successfully")
|
||||
}
|
||||
} else {
|
||||
log.Printf("[Heartbeat] Failed to get system metrics for immediate check-in: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("[Heartbeat] Rapid polling enabled successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleDisableHeartbeat handles disable_heartbeat command
|
||||
func handleDisableHeartbeat(apiClient *client.Client, cfg *config.Config, commandID string) error {
|
||||
log.Printf("[Heartbeat] Disabling rapid polling")
|
||||
|
||||
// Update agent config to disable rapid polling
|
||||
cfg.RapidPollingEnabled = false
|
||||
cfg.RapidPollingUntil = time.Time{} // Zero value
|
||||
|
||||
// Save config to persist heartbeat settings
|
||||
if err := cfg.Save(getConfigPath()); err != nil {
|
||||
log.Printf("[Heartbeat] Warning: Failed to save config: %v", err)
|
||||
}
|
||||
|
||||
// Create log report for heartbeat disable
|
||||
logReport := client.LogReport{
|
||||
CommandID: commandID,
|
||||
Action: "disable_heartbeat",
|
||||
Result: "success",
|
||||
Stdout: "Heartbeat disabled",
|
||||
Stderr: "",
|
||||
ExitCode: 0,
|
||||
DurationSeconds: 0,
|
||||
}
|
||||
|
||||
if reportErr := apiClient.ReportLog(cfg.AgentID, logReport); reportErr != nil {
|
||||
log.Printf("[Heartbeat] Failed to report heartbeat disable: %v", reportErr)
|
||||
}
|
||||
|
||||
// Send immediate check-in to update heartbeat status in UI
|
||||
log.Printf("[Heartbeat] Sending immediate check-in to update status")
|
||||
sysMetrics, err := system.GetLightweightMetrics()
|
||||
if err == nil {
|
||||
metrics := &client.SystemMetrics{
|
||||
CPUPercent: sysMetrics.CPUPercent,
|
||||
MemoryPercent: sysMetrics.MemoryPercent,
|
||||
MemoryUsedGB: sysMetrics.MemoryUsedGB,
|
||||
MemoryTotalGB: sysMetrics.MemoryTotalGB,
|
||||
DiskUsedGB: sysMetrics.DiskUsedGB,
|
||||
DiskTotalGB: sysMetrics.DiskTotalGB,
|
||||
DiskPercent: sysMetrics.DiskPercent,
|
||||
Uptime: sysMetrics.Uptime,
|
||||
Version: AgentVersion,
|
||||
}
|
||||
// Include empty heartbeat metadata to explicitly show disabled state
|
||||
metrics.Metadata = map[string]interface{}{
|
||||
"rapid_polling_enabled": false,
|
||||
"rapid_polling_until": "",
|
||||
}
|
||||
|
||||
// Send immediate check-in with updated heartbeat status
|
||||
_, checkinErr := apiClient.GetCommands(cfg.AgentID, metrics)
|
||||
if checkinErr != nil {
|
||||
log.Printf("[Heartbeat] Failed to send immediate check-in: %v", checkinErr)
|
||||
} else {
|
||||
log.Printf("[Heartbeat] Immediate check-in sent successfully")
|
||||
}
|
||||
} else {
|
||||
log.Printf("[Heartbeat] Failed to get system metrics for immediate check-in: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("[Heartbeat] Rapid polling disabled successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// reportSystemInfo collects and reports detailed system information to the server
|
||||
func reportSystemInfo(apiClient *client.Client, cfg *config.Config) error {
|
||||
// Collect detailed system information
|
||||
|
||||
@@ -29,6 +29,16 @@ create_user() {
|
||||
useradd -r -s /bin/false -d "$AGENT_HOME" -m "$AGENT_USER"
|
||||
echo "✓ User $AGENT_USER created"
|
||||
fi
|
||||
|
||||
# Add user to docker group for Docker update scanning
|
||||
if getent group docker &>/dev/null; then
|
||||
echo "Adding $AGENT_USER to docker group..."
|
||||
usermod -aG docker "$AGENT_USER"
|
||||
echo "✓ User $AGENT_USER added to docker group"
|
||||
else
|
||||
echo "⚠ Docker group not found - Docker updates will not be available"
|
||||
echo " (Install Docker first, then reinstall the agent to enable Docker support)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to build agent binary
|
||||
@@ -58,19 +68,19 @@ install_sudoers() {
|
||||
# APT package management commands
|
||||
redflag-agent ALL=(root) NOPASSWD: /usr/bin/apt-get update
|
||||
redflag-agent ALL=(root) NOPASSWD: /usr/bin/apt-get install -y *
|
||||
redflag-agent ALL=(root) NOPASSWD: /usr/bin/apt-get upgrade -y
|
||||
redflag-agent ALL=(root) NOPASSWD: /usr/bin/apt-get upgrade -y *
|
||||
redflag-agent ALL=(root) NOPASSWD: /usr/bin/apt-get install --dry-run --yes *
|
||||
|
||||
# DNF package management commands
|
||||
redflag-agent ALL=(root) NOPASSWD: /usr/bin/dnf refresh -y
|
||||
redflag-agent ALL=(root) NOPASSWD: /usr/bin/dnf makecache
|
||||
redflag-agent ALL=(root) NOPASSWD: /usr/bin/dnf install -y *
|
||||
redflag-agent ALL=(root) NOPASSWD: /usr/bin/dnf upgrade -y
|
||||
redflag-agent ALL=(root) NOPASSWD: /usr/bin/dnf upgrade -y *
|
||||
redflag-agent ALL=(root) NOPASSWD: /usr/bin/dnf install --assumeno --downloadonly *
|
||||
|
||||
# Docker operations (uncomment if needed)
|
||||
# redflag-agent ALL=(root) NOPASSWD: /usr/bin/docker pull *
|
||||
# redflag-agent ALL=(root) NOPASSWD: /usr/bin/docker image inspect *
|
||||
# redflag-agent ALL=(root) NOPASSWD: /usr/bin/docker manifest inspect *
|
||||
# Docker operations
|
||||
redflag-agent ALL=(root) NOPASSWD: /usr/bin/docker pull *
|
||||
redflag-agent ALL=(root) NOPASSWD: /usr/bin/docker image inspect *
|
||||
redflag-agent ALL=(root) NOPASSWD: /usr/bin/docker manifest inspect *
|
||||
EOF
|
||||
|
||||
chmod 440 "$SUDOERS_FILE"
|
||||
@@ -103,10 +113,10 @@ Restart=always
|
||||
RestartSec=30
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=true
|
||||
# NoNewPrivileges=true - DISABLED: Prevents sudo from working
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths=$AGENT_HOME
|
||||
ReadWritePaths=$AGENT_HOME /var/log /etc/aggregator
|
||||
PrivateTmp=true
|
||||
|
||||
[Install]
|
||||
|
||||
@@ -16,9 +16,11 @@ import (
|
||||
|
||||
// Client handles API communication with the server
|
||||
type Client struct {
|
||||
baseURL string
|
||||
token string
|
||||
http *http.Client
|
||||
baseURL string
|
||||
token string
|
||||
http *http.Client
|
||||
RapidPollingEnabled bool
|
||||
RapidPollingUntil time.Time
|
||||
}
|
||||
|
||||
// NewClient creates a new API client
|
||||
@@ -159,20 +161,28 @@ type Command struct {
|
||||
|
||||
// CommandsResponse contains pending commands
|
||||
type CommandsResponse struct {
|
||||
Commands []Command `json:"commands"`
|
||||
Commands []Command `json:"commands"`
|
||||
RapidPolling *RapidPollingConfig `json:"rapid_polling,omitempty"`
|
||||
}
|
||||
|
||||
// RapidPollingConfig contains rapid polling configuration from server
|
||||
type RapidPollingConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Until string `json:"until"` // ISO 8601 timestamp
|
||||
}
|
||||
|
||||
// SystemMetrics represents lightweight system metrics sent with check-ins
|
||||
type SystemMetrics struct {
|
||||
CPUPercent float64 `json:"cpu_percent,omitempty"`
|
||||
MemoryPercent float64 `json:"memory_percent,omitempty"`
|
||||
MemoryUsedGB float64 `json:"memory_used_gb,omitempty"`
|
||||
MemoryTotalGB float64 `json:"memory_total_gb,omitempty"`
|
||||
DiskUsedGB float64 `json:"disk_used_gb,omitempty"`
|
||||
DiskTotalGB float64 `json:"disk_total_gb,omitempty"`
|
||||
DiskPercent float64 `json:"disk_percent,omitempty"`
|
||||
Uptime string `json:"uptime,omitempty"`
|
||||
Version string `json:"version,omitempty"` // Agent version
|
||||
CPUPercent float64 `json:"cpu_percent,omitempty"`
|
||||
MemoryPercent float64 `json:"memory_percent,omitempty"`
|
||||
MemoryUsedGB float64 `json:"memory_used_gb,omitempty"`
|
||||
MemoryTotalGB float64 `json:"memory_total_gb,omitempty"`
|
||||
DiskUsedGB float64 `json:"disk_used_gb,omitempty"`
|
||||
DiskTotalGB float64 `json:"disk_total_gb,omitempty"`
|
||||
DiskPercent float64 `json:"disk_percent,omitempty"`
|
||||
Uptime string `json:"uptime,omitempty"`
|
||||
Version string `json:"version,omitempty"` // Agent version
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"` // Additional metadata
|
||||
}
|
||||
|
||||
// GetCommands retrieves pending commands from the server
|
||||
@@ -219,6 +229,16 @@ func (c *Client) GetCommands(agentID uuid.UUID, metrics *SystemMetrics) ([]Comma
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Handle rapid polling configuration if provided
|
||||
if result.RapidPolling != nil {
|
||||
// Parse the timestamp
|
||||
if until, err := time.Parse(time.RFC3339, result.RapidPolling.Until); err == nil {
|
||||
// Update client's rapid polling configuration
|
||||
c.RapidPollingEnabled = result.RapidPolling.Enabled
|
||||
c.RapidPollingUntil = until
|
||||
}
|
||||
}
|
||||
|
||||
return result.Commands, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -5,21 +5,152 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Config holds agent configuration
|
||||
type Config struct {
|
||||
ServerURL string `json:"server_url"`
|
||||
AgentID uuid.UUID `json:"agent_id"`
|
||||
Token string `json:"token"` // Short-lived access token (24h)
|
||||
RefreshToken string `json:"refresh_token"` // Long-lived refresh token (90d)
|
||||
CheckInInterval int `json:"check_in_interval"`
|
||||
// ProxyConfig holds proxy configuration
|
||||
type ProxyConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
HTTP string `json:"http,omitempty"` // HTTP proxy URL
|
||||
HTTPS string `json:"https,omitempty"` // HTTPS proxy URL
|
||||
NoProxy string `json:"no_proxy,omitempty"` // Comma-separated hosts to bypass proxy
|
||||
Username string `json:"username,omitempty"` // Proxy username (optional)
|
||||
Password string `json:"password,omitempty"` // Proxy password (optional)
|
||||
}
|
||||
|
||||
// Load reads configuration from file
|
||||
func Load(configPath string) (*Config, error) {
|
||||
// TLSConfig holds TLS/security configuration
|
||||
type TLSConfig struct {
|
||||
InsecureSkipVerify bool `json:"insecure_skip_verify"` // Skip TLS certificate verification
|
||||
CertFile string `json:"cert_file,omitempty"` // Client certificate file
|
||||
KeyFile string `json:"key_file,omitempty"` // Client key file
|
||||
CAFile string `json:"ca_file,omitempty"` // CA certificate file
|
||||
}
|
||||
|
||||
// NetworkConfig holds network-related configuration
|
||||
type NetworkConfig struct {
|
||||
Timeout time.Duration `json:"timeout"` // Request timeout
|
||||
RetryCount int `json:"retry_count"` // Number of retries
|
||||
RetryDelay time.Duration `json:"retry_delay"` // Delay between retries
|
||||
MaxIdleConn int `json:"max_idle_conn"` // Maximum idle connections
|
||||
}
|
||||
|
||||
// LoggingConfig holds logging configuration
|
||||
type LoggingConfig struct {
|
||||
Level string `json:"level"` // Log level (debug, info, warn, error)
|
||||
File string `json:"file,omitempty"` // Log file path (optional)
|
||||
MaxSize int `json:"max_size"` // Max log file size in MB
|
||||
MaxBackups int `json:"max_backups"` // Max number of log file backups
|
||||
MaxAge int `json:"max_age"` // Max age of log files in days
|
||||
}
|
||||
|
||||
// Config holds agent configuration
|
||||
type Config struct {
|
||||
// Server Configuration
|
||||
ServerURL string `json:"server_url"`
|
||||
RegistrationToken string `json:"registration_token,omitempty"` // One-time registration token
|
||||
|
||||
// Agent Authentication
|
||||
AgentID uuid.UUID `json:"agent_id"`
|
||||
Token string `json:"token"` // Short-lived access token (24h)
|
||||
RefreshToken string `json:"refresh_token"` // Long-lived refresh token (90d)
|
||||
|
||||
// Agent Behavior
|
||||
CheckInInterval int `json:"check_in_interval"`
|
||||
|
||||
// Rapid polling mode for faster response during operations
|
||||
RapidPollingEnabled bool `json:"rapid_polling_enabled"`
|
||||
RapidPollingUntil time.Time `json:"rapid_polling_until"`
|
||||
|
||||
// Network Configuration
|
||||
Network NetworkConfig `json:"network,omitempty"`
|
||||
|
||||
// Proxy Configuration
|
||||
Proxy ProxyConfig `json:"proxy,omitempty"`
|
||||
|
||||
// Security Configuration
|
||||
TLS TLSConfig `json:"tls,omitempty"`
|
||||
|
||||
// Logging Configuration
|
||||
Logging LoggingConfig `json:"logging,omitempty"`
|
||||
|
||||
// Agent Metadata
|
||||
Tags []string `json:"tags,omitempty"` // User-defined tags
|
||||
Metadata map[string]string `json:"metadata,omitempty"` // Custom metadata
|
||||
DisplayName string `json:"display_name,omitempty"` // Human-readable name
|
||||
Organization string `json:"organization,omitempty"` // Organization/group
|
||||
}
|
||||
|
||||
// Load reads configuration from multiple sources with priority order:
|
||||
// 1. CLI flags
|
||||
// 2. Environment variables
|
||||
// 3. Configuration file
|
||||
// 4. Default values
|
||||
func Load(configPath string, cliFlags *CLIFlags) (*Config, error) {
|
||||
// Start with defaults
|
||||
config := getDefaultConfig()
|
||||
|
||||
// Load from config file if it exists
|
||||
if fileConfig, err := loadFromFile(configPath); err == nil {
|
||||
mergeConfig(config, fileConfig)
|
||||
}
|
||||
|
||||
// Override with environment variables
|
||||
mergeConfig(config, loadFromEnv())
|
||||
|
||||
// Override with CLI flags (highest priority)
|
||||
if cliFlags != nil {
|
||||
mergeConfig(config, loadFromFlags(cliFlags))
|
||||
}
|
||||
|
||||
// Validate configuration
|
||||
if err := validateConfig(config); err != nil {
|
||||
return nil, fmt.Errorf("invalid configuration: %w", err)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// CLIFlags holds command line flag values
|
||||
type CLIFlags struct {
|
||||
ServerURL string
|
||||
RegistrationToken string
|
||||
ProxyHTTP string
|
||||
ProxyHTTPS string
|
||||
ProxyNoProxy string
|
||||
LogLevel string
|
||||
ConfigFile string
|
||||
Tags []string
|
||||
Organization string
|
||||
DisplayName string
|
||||
InsecureTLS bool
|
||||
}
|
||||
|
||||
// getDefaultConfig returns default configuration values
|
||||
func getDefaultConfig() *Config {
|
||||
return &Config{
|
||||
ServerURL: "http://localhost:8080",
|
||||
CheckInInterval: 300, // 5 minutes
|
||||
Network: NetworkConfig{
|
||||
Timeout: 30 * time.Second,
|
||||
RetryCount: 3,
|
||||
RetryDelay: 5 * time.Second,
|
||||
MaxIdleConn: 10,
|
||||
},
|
||||
Logging: LoggingConfig{
|
||||
Level: "info",
|
||||
MaxSize: 100, // 100MB
|
||||
MaxBackups: 3,
|
||||
MaxAge: 28, // 28 days
|
||||
},
|
||||
Tags: []string{},
|
||||
Metadata: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
// loadFromFile reads configuration from file
|
||||
func loadFromFile(configPath string) (*Config, error) {
|
||||
// Ensure directory exists
|
||||
dir := filepath.Dir(configPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
@@ -30,8 +161,7 @@ func Load(configPath string) (*Config, error) {
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// Return empty config if file doesn't exist
|
||||
return &Config{}, nil
|
||||
return getDefaultConfig(), nil // Return defaults if file doesn't exist
|
||||
}
|
||||
return nil, fmt.Errorf("failed to read config: %w", err)
|
||||
}
|
||||
@@ -44,6 +174,174 @@ func Load(configPath string) (*Config, error) {
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// loadFromEnv loads configuration from environment variables
|
||||
func loadFromEnv() *Config {
|
||||
config := &Config{}
|
||||
|
||||
if serverURL := os.Getenv("REDFLAG_SERVER_URL"); serverURL != "" {
|
||||
config.ServerURL = serverURL
|
||||
}
|
||||
if token := os.Getenv("REDFLAG_REGISTRATION_TOKEN"); token != "" {
|
||||
config.RegistrationToken = token
|
||||
}
|
||||
if proxyHTTP := os.Getenv("REDFLAG_HTTP_PROXY"); proxyHTTP != "" {
|
||||
config.Proxy.Enabled = true
|
||||
config.Proxy.HTTP = proxyHTTP
|
||||
}
|
||||
if proxyHTTPS := os.Getenv("REDFLAG_HTTPS_PROXY"); proxyHTTPS != "" {
|
||||
config.Proxy.Enabled = true
|
||||
config.Proxy.HTTPS = proxyHTTPS
|
||||
}
|
||||
if noProxy := os.Getenv("REDFLAG_NO_PROXY"); noProxy != "" {
|
||||
config.Proxy.NoProxy = noProxy
|
||||
}
|
||||
if logLevel := os.Getenv("REDFLAG_LOG_LEVEL"); logLevel != "" {
|
||||
if config.Logging == (LoggingConfig{}) {
|
||||
config.Logging = LoggingConfig{}
|
||||
}
|
||||
config.Logging.Level = logLevel
|
||||
}
|
||||
if org := os.Getenv("REDFLAG_ORGANIZATION"); org != "" {
|
||||
config.Organization = org
|
||||
}
|
||||
if displayName := os.Getenv("REDFLAG_DISPLAY_NAME"); displayName != "" {
|
||||
config.DisplayName = displayName
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// loadFromFlags loads configuration from CLI flags
|
||||
func loadFromFlags(flags *CLIFlags) *Config {
|
||||
config := &Config{}
|
||||
|
||||
if flags.ServerURL != "" {
|
||||
config.ServerURL = flags.ServerURL
|
||||
}
|
||||
if flags.RegistrationToken != "" {
|
||||
config.RegistrationToken = flags.RegistrationToken
|
||||
}
|
||||
if flags.ProxyHTTP != "" || flags.ProxyHTTPS != "" {
|
||||
config.Proxy = ProxyConfig{
|
||||
Enabled: true,
|
||||
HTTP: flags.ProxyHTTP,
|
||||
HTTPS: flags.ProxyHTTPS,
|
||||
NoProxy: flags.ProxyNoProxy,
|
||||
}
|
||||
}
|
||||
if flags.LogLevel != "" {
|
||||
config.Logging = LoggingConfig{
|
||||
Level: flags.LogLevel,
|
||||
}
|
||||
}
|
||||
if len(flags.Tags) > 0 {
|
||||
config.Tags = flags.Tags
|
||||
}
|
||||
if flags.Organization != "" {
|
||||
config.Organization = flags.Organization
|
||||
}
|
||||
if flags.DisplayName != "" {
|
||||
config.DisplayName = flags.DisplayName
|
||||
}
|
||||
if flags.InsecureTLS {
|
||||
config.TLS = TLSConfig{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// mergeConfig merges source config into target config (non-zero values only)
|
||||
func mergeConfig(target, source *Config) {
|
||||
if source.ServerURL != "" {
|
||||
target.ServerURL = source.ServerURL
|
||||
}
|
||||
if source.RegistrationToken != "" {
|
||||
target.RegistrationToken = source.RegistrationToken
|
||||
}
|
||||
if source.CheckInInterval != 0 {
|
||||
target.CheckInInterval = source.CheckInInterval
|
||||
}
|
||||
if source.AgentID != uuid.Nil {
|
||||
target.AgentID = source.AgentID
|
||||
}
|
||||
if source.Token != "" {
|
||||
target.Token = source.Token
|
||||
}
|
||||
if source.RefreshToken != "" {
|
||||
target.RefreshToken = source.RefreshToken
|
||||
}
|
||||
|
||||
// Merge nested configs
|
||||
if source.Network != (NetworkConfig{}) {
|
||||
target.Network = source.Network
|
||||
}
|
||||
if source.Proxy != (ProxyConfig{}) {
|
||||
target.Proxy = source.Proxy
|
||||
}
|
||||
if source.TLS != (TLSConfig{}) {
|
||||
target.TLS = source.TLS
|
||||
}
|
||||
if source.Logging != (LoggingConfig{}) {
|
||||
target.Logging = source.Logging
|
||||
}
|
||||
|
||||
// Merge metadata
|
||||
if source.Tags != nil {
|
||||
target.Tags = source.Tags
|
||||
}
|
||||
if source.Metadata != nil {
|
||||
if target.Metadata == nil {
|
||||
target.Metadata = make(map[string]string)
|
||||
}
|
||||
for k, v := range source.Metadata {
|
||||
target.Metadata[k] = v
|
||||
}
|
||||
}
|
||||
if source.DisplayName != "" {
|
||||
target.DisplayName = source.DisplayName
|
||||
}
|
||||
if source.Organization != "" {
|
||||
target.Organization = source.Organization
|
||||
}
|
||||
|
||||
// Merge rapid polling settings
|
||||
target.RapidPollingEnabled = source.RapidPollingEnabled
|
||||
if !source.RapidPollingUntil.IsZero() {
|
||||
target.RapidPollingUntil = source.RapidPollingUntil
|
||||
}
|
||||
}
|
||||
|
||||
// validateConfig validates configuration values
|
||||
func validateConfig(config *Config) error {
|
||||
if config.ServerURL == "" {
|
||||
return fmt.Errorf("server_url is required")
|
||||
}
|
||||
if config.CheckInInterval < 30 {
|
||||
return fmt.Errorf("check_in_interval must be at least 30 seconds")
|
||||
}
|
||||
if config.CheckInInterval > 3600 {
|
||||
return fmt.Errorf("check_in_interval cannot exceed 3600 seconds (1 hour)")
|
||||
}
|
||||
if config.Network.Timeout <= 0 {
|
||||
return fmt.Errorf("network timeout must be positive")
|
||||
}
|
||||
if config.Network.RetryCount < 0 || config.Network.RetryCount > 10 {
|
||||
return fmt.Errorf("retry_count must be between 0 and 10")
|
||||
}
|
||||
|
||||
// Validate log level
|
||||
validLogLevels := map[string]bool{
|
||||
"debug": true, "info": true, "warn": true, "error": true,
|
||||
}
|
||||
if config.Logging.Level != "" && !validLogLevels[config.Logging.Level] {
|
||||
return fmt.Errorf("invalid log level: %s", config.Logging.Level)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save writes configuration to file
|
||||
func (c *Config) Save(configPath string) error {
|
||||
data, err := json.MarshalIndent(c, "", " ")
|
||||
@@ -62,3 +360,13 @@ func (c *Config) Save(configPath string) error {
|
||||
func (c *Config) IsRegistered() bool {
|
||||
return c.AgentID != uuid.Nil && c.Token != ""
|
||||
}
|
||||
|
||||
// NeedsRegistration checks if the agent needs to register with a token
|
||||
func (c *Config) NeedsRegistration() bool {
|
||||
return c.RegistrationToken != "" && c.AgentID == uuid.Nil
|
||||
}
|
||||
|
||||
// HasRegistrationToken checks if the agent has a registration token
|
||||
func (c *Config) HasRegistrationToken() bool {
|
||||
return c.RegistrationToken != ""
|
||||
}
|
||||
|
||||
@@ -146,6 +146,36 @@ func (i *APTInstaller) Upgrade() (*InstallResult, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdatePackage updates a specific package using APT
|
||||
func (i *APTInstaller) UpdatePackage(packageName string) (*InstallResult, error) {
|
||||
startTime := time.Now()
|
||||
|
||||
// Update specific package using secure executor
|
||||
updateResult, err := i.executor.ExecuteCommand("apt-get", []string{"install", "--only-upgrade", "-y", packageName})
|
||||
duration := int(time.Since(startTime).Seconds())
|
||||
|
||||
if err != nil {
|
||||
return &InstallResult{
|
||||
Success: false,
|
||||
ErrorMessage: fmt.Sprintf("APT update failed: %v", err),
|
||||
Stdout: updateResult.Stdout,
|
||||
Stderr: updateResult.Stderr,
|
||||
ExitCode: updateResult.ExitCode,
|
||||
DurationSeconds: duration,
|
||||
}, err
|
||||
}
|
||||
|
||||
return &InstallResult{
|
||||
Success: true,
|
||||
Stdout: updateResult.Stdout,
|
||||
Stderr: updateResult.Stderr,
|
||||
ExitCode: updateResult.ExitCode,
|
||||
DurationSeconds: duration,
|
||||
PackagesInstalled: []string{packageName},
|
||||
Action: "update",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DryRun performs a dry run installation to check dependencies
|
||||
func (i *APTInstaller) DryRun(packageName string) (*InstallResult, error) {
|
||||
startTime := time.Now()
|
||||
|
||||
@@ -31,15 +31,8 @@ func (i *DNFInstaller) IsAvailable() bool {
|
||||
func (i *DNFInstaller) Install(packageName string) (*InstallResult, error) {
|
||||
startTime := time.Now()
|
||||
|
||||
// Refresh package cache first using secure executor
|
||||
refreshResult, err := i.executor.ExecuteCommand("dnf", []string{"makecache"})
|
||||
if err != nil {
|
||||
refreshResult.DurationSeconds = int(time.Since(startTime).Seconds())
|
||||
refreshResult.ErrorMessage = fmt.Sprintf("Failed to refresh DNF cache: %v", err)
|
||||
return refreshResult, fmt.Errorf("dnf refresh failed: %w", err)
|
||||
}
|
||||
|
||||
// Install package using secure executor
|
||||
// For single package installs, skip makecache to avoid repository conflicts
|
||||
// Only run makecache when installing multiple packages (InstallMultiple method)
|
||||
installResult, err := i.executor.ExecuteCommand("dnf", []string{"install", "-y", packageName})
|
||||
duration := int(time.Since(startTime).Seconds())
|
||||
|
||||
@@ -75,17 +68,11 @@ func (i *DNFInstaller) InstallMultiple(packageNames []string) (*InstallResult, e
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
// Refresh package cache first using secure executor
|
||||
refreshResult, err := i.executor.ExecuteCommand("dnf", []string{"makecache"})
|
||||
if err != nil {
|
||||
refreshResult.DurationSeconds = int(time.Since(startTime).Seconds())
|
||||
refreshResult.ErrorMessage = fmt.Sprintf("Failed to refresh DNF cache: %v", err)
|
||||
return refreshResult, fmt.Errorf("dnf refresh failed: %w", err)
|
||||
}
|
||||
|
||||
// Install all packages in one command using secure executor
|
||||
args := []string{"install", "-y"}
|
||||
args = append(args, packageNames...)
|
||||
|
||||
// Install all packages in one command using secure executor
|
||||
installResult, err := i.executor.ExecuteCommand("dnf", args)
|
||||
duration := int(time.Since(startTime).Seconds())
|
||||
|
||||
@@ -299,6 +286,37 @@ func (i *DNFInstaller) extractPackageNameFromDNFLine(line string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// UpdatePackage updates a specific package using DNF
|
||||
func (i *DNFInstaller) UpdatePackage(packageName string) (*InstallResult, error) {
|
||||
startTime := time.Now()
|
||||
|
||||
// Update specific package using secure executor
|
||||
// Use 'dnf upgrade' instead of 'dnf install' for existing packages
|
||||
updateResult, err := i.executor.ExecuteCommand("dnf", []string{"upgrade", "-y", packageName})
|
||||
duration := int(time.Since(startTime).Seconds())
|
||||
|
||||
if err != nil {
|
||||
return &InstallResult{
|
||||
Success: false,
|
||||
ErrorMessage: fmt.Sprintf("DNF upgrade failed: %v", err),
|
||||
Stdout: updateResult.Stdout,
|
||||
Stderr: updateResult.Stderr,
|
||||
ExitCode: updateResult.ExitCode,
|
||||
DurationSeconds: duration,
|
||||
}, err
|
||||
}
|
||||
|
||||
return &InstallResult{
|
||||
Success: true,
|
||||
Stdout: updateResult.Stdout,
|
||||
Stderr: updateResult.Stderr,
|
||||
ExitCode: updateResult.ExitCode,
|
||||
DurationSeconds: duration,
|
||||
PackagesInstalled: []string{packageName},
|
||||
Action: "upgrade",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetPackageType returns type of packages this installer handles
|
||||
func (i *DNFInstaller) GetPackageType() string {
|
||||
return "dnf"
|
||||
|
||||
@@ -60,6 +60,12 @@ func (i *DockerInstaller) Update(imageName, targetVersion string) (*InstallResul
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdatePackage updates a specific Docker image (alias for Update method)
|
||||
func (i *DockerInstaller) UpdatePackage(imageName string) (*InstallResult, error) {
|
||||
// Docker uses same logic for updating as installing
|
||||
return i.Update(imageName, "")
|
||||
}
|
||||
|
||||
// Install installs a Docker image (alias for Update)
|
||||
func (i *DockerInstaller) Install(imageName string) (*InstallResult, error) {
|
||||
return i.Update(imageName, "")
|
||||
|
||||
@@ -8,6 +8,7 @@ type Installer interface {
|
||||
Install(packageName string) (*InstallResult, error)
|
||||
InstallMultiple(packageNames []string) (*InstallResult, error)
|
||||
Upgrade() (*InstallResult, error)
|
||||
UpdatePackage(packageName string) (*InstallResult, error) // New: Update specific package
|
||||
GetPackageType() string
|
||||
DryRun(packageName string) (*InstallResult, error) // New: Perform dry run to check dependencies
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ var AllowedCommands = map[string][]string{
|
||||
},
|
||||
"dnf": {
|
||||
"refresh",
|
||||
"makecache",
|
||||
"install",
|
||||
"upgrade",
|
||||
},
|
||||
@@ -93,6 +94,9 @@ func (e *SecureCommandExecutor) validateDNFCommand(args []string) error {
|
||||
if !contains(args, "-y") {
|
||||
return fmt.Errorf("dnf refresh must include -y flag")
|
||||
}
|
||||
case "makecache":
|
||||
// makecache doesn't require -y flag as it's read-only
|
||||
return nil
|
||||
case "install":
|
||||
// Allow dry-run flags for dependency checking
|
||||
dryRunFlags := []string{"--assumeno", "--downloadonly"}
|
||||
@@ -165,12 +169,22 @@ func (e *SecureCommandExecutor) ExecuteCommand(baseCmd string, args []string) (*
|
||||
}, fmt.Errorf("command validation failed: %w", err)
|
||||
}
|
||||
|
||||
// Log the command for audit purposes (in a real implementation, this would go to a secure log)
|
||||
fmt.Printf("[AUDIT] Executing command: %s %s\n", baseCmd, strings.Join(args, " "))
|
||||
// Resolve the full path to the command (required for sudo to match sudoers rules)
|
||||
fullPath, err := exec.LookPath(baseCmd)
|
||||
if err != nil {
|
||||
return &InstallResult{
|
||||
Success: false,
|
||||
ErrorMessage: fmt.Sprintf("Command not found: %s", baseCmd),
|
||||
}, fmt.Errorf("command not found: %w", err)
|
||||
}
|
||||
|
||||
// Execute the command without sudo - it will be handled by sudoers
|
||||
fullArgs := append([]string{baseCmd}, args...)
|
||||
cmd := exec.Command(fullArgs[0], fullArgs[1:]...)
|
||||
// Log the command for audit purposes (in a real implementation, this would go to a secure log)
|
||||
fmt.Printf("[AUDIT] Executing command: sudo %s %s\n", fullPath, strings.Join(args, " "))
|
||||
|
||||
// Execute the command with sudo - requires sudoers configuration
|
||||
// Use full path to match sudoers rules exactly
|
||||
fullArgs := append([]string{fullPath}, args...)
|
||||
cmd := exec.Command("sudo", fullArgs...)
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
|
||||
@@ -117,6 +117,12 @@ func (i *WindowsUpdateInstaller) installViaPowerShell(packageNames []string) (st
|
||||
return "Windows Updates installed via PowerShell", nil
|
||||
}
|
||||
|
||||
// UpdatePackage updates a specific Windows update (alias for Install method)
|
||||
func (i *WindowsUpdateInstaller) UpdatePackage(packageName string) (*InstallResult, error) {
|
||||
// Windows uses same logic for updating as installing
|
||||
return i.Install(packageName)
|
||||
}
|
||||
|
||||
// installViaWuauclt uses traditional Windows Update client
|
||||
func (i *WindowsUpdateInstaller) installViaWuauclt(packageNames []string) (string, error) {
|
||||
// Force detection of updates
|
||||
|
||||
@@ -371,4 +371,10 @@ type WingetPackage struct {
|
||||
Source string `json:"Source"`
|
||||
IsPinned bool `json:"IsPinned"`
|
||||
PinReason string `json:"PinReason,omitempty"`
|
||||
}
|
||||
|
||||
// UpdatePackage updates a specific winget package (alias for Install method)
|
||||
func (i *WingetInstaller) UpdatePackage(packageName string) (*InstallResult, error) {
|
||||
// Winget uses same logic for updating as installing
|
||||
return i.Install(packageName)
|
||||
}
|
||||
Reference in New Issue
Block a user