Windows agent improvements and dependency workflow fixes

- Added system info reporting to agent main loop
- Updated README with current project status and screenshots
- Fixed a few workflow quirks
This commit is contained in:
Fimeg
2025-10-17 16:26:21 -04:00
parent 2ade509b63
commit 4ef5216c89
10 changed files with 537 additions and 66 deletions

120
README.md
View File

@@ -52,33 +52,20 @@ A self-hosted, cross-platform update management platform built with:
## Screenshots
### Main Dashboard
![Main Dashboard](Screenshots/RedFlag%20Default%20Dashboard.png)
Overview showing agent status, system metrics, and update statistics
| Overview | Updates Management | Agent List |
|----------|-------------------|------------|
| ![Main Dashboard](Screenshots/RedFlag%20Default%20Dashboard.png) | ![Updates Dashboard](Screenshots/RedFlag%20Updates%20Dashboard.png) | ![Agent List](Screenshots/RedFlag%20Agent%20List.png) |
| System overview with metrics | Update approval with dependency workflow | Cross-platform agent management |
### Updates Management
![Updates Dashboard](Screenshots/RedFlag%20Updates%20Dashboard.png)
Comprehensive update listing with filtering, approval, and dependency confirmation
| Linux Agent Details | Windows Agent Details | History & Audit |
|-------------------|---------------------|----------------|
| ![Linux Agent Details](Screenshots/RedFlag%20Linux%20Agent%20Details.png) | ![Windows Agent Details](Screenshots/RedFlag%20Windows%20Agent%20Details.png) | ![History Dashboard](Screenshots/RedFlag%20History%20Dashboard.png) |
| Linux system specs and updates | Windows Updates and Winget support | Complete audit trail of activities |
### Agent Details
![Agent Details](Screenshots/RedFlag%20Agent%20Details.png)
Detailed agent information including system specs, last check-in, and individual update management
### Windows Agent Support
![Windows Agent](Screenshots/RedFlag%20Windows%20Agent%20Details.png)
Cross-platform support for Windows Updates and Winget package management
### History & Audit Trail
![History Dashboard](Screenshots/RedFlag%20History%20Dashboard.png)
Complete audit trail of all update activities and command execution
### Live Operations
![Live Operations](Screenshots/RedFlag%20Live%20Operations%20-%20Failed%20Dashboard.png)
Real-time view of update operations with success/failure tracking
### Docker Container Management
![Docker Dashboard](Screenshots/RedFlag%20Docker%20Dashboard.png)
Docker-specific interface for container image updates and management
| Live Operations | Docker Management |
|-----------------|------------------|
| ![Live Operations](Screenshots/RedFlag%20Live%20Operations%20-%20Failed%20Dashboard.png) | ![Docker Dashboard](Screenshots/RedFlag%20Docker%20Dashboard.png) |
| Real-time operation tracking | Container image update management |
## For Developers
@@ -104,9 +91,9 @@ This repository contains:
┌────┴────┬────────┐
│ │ │
┌───▼──┐ ┌──▼──┐ ┌──▼───┐
│Linux │ │Linux│ │Linux │
│Agent │ │Agent│ │Agent │
└──────┘ └─────┘ └──────┘
│Linux │ │Windows│ │Linux │
│Agent │ │Agent │ │Agent │
└──────┘ └───────┘ └──────┘
```
## Project Structure
@@ -117,19 +104,45 @@ RedFlag/
│ ├── cmd/server/ # Main entry point
│ ├── internal/
│ │ ├── api/ # HTTP handlers & middleware
│ │ │ └── handlers/ # API endpoint handlers
│ │ ├── database/ # Database layer & migrations
│ │ ├── models/ # Data models
│ │ └── config/ # Configuration
│ │ ├── migrations/ # Database schema migrations
│ │ │ └── queries/ # Database query functions
│ │ ├── models/ # Data models and structs
│ │ ├── services/ # Business logic services
│ │ ├── utils/ # Utility functions
│ │ └── config/ # Configuration management
│ └── go.mod
├── aggregator-agent/ # Go agent
├── aggregator-agent/ # Go agent (cross-platform)
│ ├── cmd/agent/ # Main entry point
│ ├── internal/
│ │ ├── client/ # API client
│ │ ├── installer/ # Update installers (APT, DNF, Docker)
│ │ ├── scanner/ # Update scanners (APT, Docker, DNF/RPM)
│ │ ├── cache/ # Local cache system for offline viewing
│ │ ├── client/ # API client with token renewal
│ │ ├── config/ # Configuration management
│ │ ├── display/ # Terminal output formatting
│ │ ├── installer/ # Update installers
│ │ │ ├── apt.go # APT package installer
│ │ │ ├── dnf.go # DNF package installer
│ │ │ ├── docker.go # Docker image installer
│ │ │ ├── windows.go # Windows installer base
│ │ │ ├── winget.go # Winget package installer
│ │ │ ├── security.go # Security utilities
│ │ │ └── sudoers.go # Sudo management
│ │ ├── scanner/ # Update scanners
│ │ │ ├── apt.go # APT package scanner
│ │ │ ├── dnf.go # DNF package scanner
│ │ │ ├── docker.go # Docker image scanner
│ │ │ ├── registry.go # Docker registry client
│ │ │ ├── windows.go # Windows Update scanner
│ │ │ ├── winget.go # Winget package scanner
│ │ │ └── windows_*.go # Windows Update API components
│ │ ├── system/ # System information collection
│ │ └── config/ # Configuration
│ │ │ ├── info.go # System metrics
│ │ │ └── windows.go # Windows system info
│ │ └── executor/ # Command execution
│ ├── install.sh # Linux installation script
│ ├── uninstall.sh # Linux uninstallation script
│ └── go.mod
├── aggregator-web/ # React dashboard
@@ -141,10 +154,12 @@ RedFlag/
## Database Schema
Key Tables:
- `agents` - Registered agents with system metadata
- `update_packages` - Discovered updates
- `agent_commands` - Command queue for agents
- `update_logs` - Execution logs
- `agents` - Registered agents with system metadata and version tracking
- `refresh_tokens` - Long-lived refresh tokens for stable agent identity
- `update_events` - Immutable event storage for update discoveries
- `current_package_state` - Optimized view of current update state
- `agent_commands` - Command queue for agents (scan, install, dry-run)
- `update_logs` - Execution logs with detailed results
- `agent_tags` - Agent tagging/grouping
## Configuration
@@ -164,7 +179,8 @@ Auto-generated on registration:
{
"server_url": "http://localhost:8080",
"agent_id": "uuid",
"token": "jwt-token",
"token": "jwt-access-token",
"refresh_token": "long-lived-refresh-token",
"check_in_interval": 300
}
```
@@ -219,12 +235,34 @@ curl http://localhost:8080/api/v1/updates?status=pending
curl -X POST http://localhost:8080/api/v1/updates/{update-id}/approve
```
### Token Renewal (Agent Authentication)
```bash
# Exchange refresh token for new access token
curl -X POST http://localhost:8080/api/v1/agents/renew \
-H "Content-Type: application/json" \
-d '{
"agent_id": "uuid",
"refresh_token": "long-lived-token"
}'
```
### Dependency Workflow
```bash
# Dry run to check dependencies (automatically triggered by install)
curl -X POST http://localhost:8080/api/v1/updates/{update-id}/approve
# Confirm dependencies and install
curl -X POST http://localhost:8080/api/v1/updates/{update-id}/confirm-dependencies
```
## Security
- Agent Authentication: JWT tokens with 24h expiry
- Agent Authentication: Refresh token system with 90-day sliding window + 24h access tokens
- SHA-256 token hashing for secure storage
- Pull-based Model: Agents poll server (firewall-friendly)
- Command Validation: Whitelisted commands only
- TLS Required: Production deployments must use HTTPS
- Token Renewal: `/renew` endpoint prevents daily re-registration
## License

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 107 KiB

View File

@@ -273,12 +273,27 @@ func runAgent(cfg *config.Config) error {
windowsUpdateScanner := scanner.NewWindowsUpdateScanner()
wingetScanner := scanner.NewWingetScanner()
// System info tracking
var lastSystemInfoUpdate time.Time
const systemInfoUpdateInterval = 1 * time.Hour // Update detailed system info every hour
// Main check-in loop
for {
// Add jitter to prevent thundering herd
jitter := time.Duration(rand.Intn(30)) * time.Second
time.Sleep(jitter)
// Check if we need to send detailed system info update
if time.Since(lastSystemInfoUpdate) >= systemInfoUpdateInterval {
log.Printf("Updating detailed system information...")
if err := reportSystemInfo(apiClient, cfg); err != nil {
log.Printf("Failed to report system info: %v\n", err)
} else {
lastSystemInfoUpdate = time.Now()
log.Printf("✓ System information updated\n")
}
}
log.Printf("Checking in with server... (Agent v%s)", AgentVersion)
// Collect lightweight system metrics
@@ -374,17 +389,25 @@ func handleScanUpdates(apiClient *client.Client, cfg *config.Config, aptScanner
log.Println("Scanning for updates...")
var allUpdates []client.UpdateReportItem
var scanErrors []string
var scanResults []string
// Scan APT updates
if aptScanner.IsAvailable() {
log.Println(" - Scanning APT packages...")
updates, err := aptScanner.Scan()
if err != nil {
log.Printf(" APT scan failed: %v\n", err)
errorMsg := fmt.Sprintf("APT scan failed: %v", err)
log.Printf(" %s\n", errorMsg)
scanErrors = append(scanErrors, errorMsg)
} else {
log.Printf(" Found %d APT updates\n", len(updates))
resultMsg := fmt.Sprintf("Found %d APT updates", len(updates))
log.Printf(" %s\n", resultMsg)
scanResults = append(scanResults, resultMsg)
allUpdates = append(allUpdates, updates...)
}
} else {
scanResults = append(scanResults, "APT scanner not available")
}
// Scan DNF updates
@@ -392,11 +415,17 @@ func handleScanUpdates(apiClient *client.Client, cfg *config.Config, aptScanner
log.Println(" - Scanning DNF packages...")
updates, err := dnfScanner.Scan()
if err != nil {
log.Printf(" DNF scan failed: %v\n", err)
errorMsg := fmt.Sprintf("DNF scan failed: %v", err)
log.Printf(" %s\n", errorMsg)
scanErrors = append(scanErrors, errorMsg)
} else {
log.Printf(" Found %d DNF updates\n", len(updates))
resultMsg := fmt.Sprintf("Found %d DNF updates", len(updates))
log.Printf(" %s\n", resultMsg)
scanResults = append(scanResults, resultMsg)
allUpdates = append(allUpdates, updates...)
}
} else {
scanResults = append(scanResults, "DNF scanner not available")
}
// Scan Docker updates
@@ -404,11 +433,17 @@ func handleScanUpdates(apiClient *client.Client, cfg *config.Config, aptScanner
log.Println(" - Scanning Docker images...")
updates, err := dockerScanner.Scan()
if err != nil {
log.Printf(" Docker scan failed: %v\n", err)
errorMsg := fmt.Sprintf("Docker scan failed: %v", err)
log.Printf(" %s\n", errorMsg)
scanErrors = append(scanErrors, errorMsg)
} else {
log.Printf(" Found %d Docker image updates\n", len(updates))
resultMsg := fmt.Sprintf("Found %d Docker image updates", len(updates))
log.Printf(" %s\n", resultMsg)
scanResults = append(scanResults, resultMsg)
allUpdates = append(allUpdates, updates...)
}
} else {
scanResults = append(scanResults, "Docker scanner not available")
}
// Scan Windows updates
@@ -416,11 +451,17 @@ func handleScanUpdates(apiClient *client.Client, cfg *config.Config, aptScanner
log.Println(" - Scanning Windows updates...")
updates, err := windowsUpdateScanner.Scan()
if err != nil {
log.Printf(" Windows Update scan failed: %v\n", err)
errorMsg := fmt.Sprintf("Windows Update scan failed: %v", err)
log.Printf(" %s\n", errorMsg)
scanErrors = append(scanErrors, errorMsg)
} else {
log.Printf(" Found %d Windows updates\n", len(updates))
resultMsg := fmt.Sprintf("Found %d Windows updates", len(updates))
log.Printf(" %s\n", resultMsg)
scanResults = append(scanResults, resultMsg)
allUpdates = append(allUpdates, updates...)
}
} else {
scanResults = append(scanResults, "Windows Update scanner not available")
}
// Scan Winget packages
@@ -428,14 +469,58 @@ func handleScanUpdates(apiClient *client.Client, cfg *config.Config, aptScanner
log.Println(" - Scanning Winget packages...")
updates, err := wingetScanner.Scan()
if err != nil {
log.Printf(" Winget scan failed: %v\n", err)
errorMsg := fmt.Sprintf("Winget scan failed: %v", err)
log.Printf(" %s\n", errorMsg)
scanErrors = append(scanErrors, errorMsg)
} else {
log.Printf(" Found %d Winget package updates\n", len(updates))
resultMsg := fmt.Sprintf("Found %d Winget package updates", len(updates))
log.Printf(" %s\n", resultMsg)
scanResults = append(scanResults, resultMsg)
allUpdates = append(allUpdates, updates...)
}
} else {
scanResults = append(scanResults, "Winget scanner not available")
}
// Report to server
// Report scan results to server (both successes and failures)
success := len(allUpdates) > 0 || len(scanErrors) == 0
var combinedOutput string
// Combine all scan results
if len(scanResults) > 0 {
combinedOutput += "Scan Results:\n" + strings.Join(scanResults, "\n")
}
if len(scanErrors) > 0 {
if combinedOutput != "" {
combinedOutput += "\n"
}
combinedOutput += "Scan Errors:\n" + strings.Join(scanErrors, "\n")
}
if len(allUpdates) > 0 {
if combinedOutput != "" {
combinedOutput += "\n"
}
combinedOutput += fmt.Sprintf("Total Updates Found: %d", len(allUpdates))
}
// Create scan log entry
logReport := client.LogReport{
CommandID: commandID,
Action: "scan_updates",
Result: map[bool]string{true: "success", false: "failure"}[success],
Stdout: combinedOutput,
Stderr: strings.Join(scanErrors, "\n"),
ExitCode: map[bool]int{true: 0, false: 1}[success],
DurationSeconds: 0, // Could track scan duration if needed
}
// Report the scan log
if err := apiClient.ReportLog(cfg.AgentID, logReport); err != nil {
log.Printf("Failed to report scan log: %v\n", err)
// Continue anyway - updates are more important
}
// Report updates to server if any were found
if len(allUpdates) > 0 {
report := client.UpdateReport{
CommandID: commandID,
@@ -452,6 +537,11 @@ func handleScanUpdates(apiClient *client.Client, cfg *config.Config, aptScanner
log.Println("✓ No updates found")
}
// Return error if there were any scan failures
if len(scanErrors) > 0 && len(allUpdates) == 0 {
return fmt.Errorf("all scanners failed: %s", strings.Join(scanErrors, "; "))
}
return nil
}
@@ -961,6 +1051,58 @@ func handleConfirmDependencies(apiClient *client.Client, cfg *config.Config, com
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
sysInfo, err := system.GetSystemInfo(AgentVersion)
if err != nil {
return fmt.Errorf("failed to get system info: %w", err)
}
// Create system info report
report := client.SystemInfoReport{
Timestamp: time.Now(),
CPUModel: sysInfo.CPUInfo.ModelName,
CPUCores: sysInfo.CPUInfo.Cores,
CPUThreads: sysInfo.CPUInfo.Threads,
MemoryTotal: sysInfo.MemoryInfo.Total,
DiskTotal: uint64(0),
DiskUsed: uint64(0),
IPAddress: sysInfo.IPAddress,
Processes: sysInfo.RunningProcesses,
Uptime: sysInfo.Uptime,
Metadata: make(map[string]interface{}),
}
// Add primary disk info
if len(sysInfo.DiskInfo) > 0 {
primaryDisk := sysInfo.DiskInfo[0]
report.DiskTotal = primaryDisk.Total
report.DiskUsed = primaryDisk.Used
report.Metadata["disk_mount"] = primaryDisk.Mountpoint
report.Metadata["disk_filesystem"] = primaryDisk.Filesystem
}
// Add collection timestamp and additional metadata
report.Metadata["collected_at"] = time.Now().Format(time.RFC3339)
report.Metadata["hostname"] = sysInfo.Hostname
report.Metadata["os_type"] = sysInfo.OSType
report.Metadata["os_version"] = sysInfo.OSVersion
report.Metadata["os_architecture"] = sysInfo.OSArchitecture
// Add any existing metadata from system info
for key, value := range sysInfo.Metadata {
report.Metadata[key] = value
}
// Report to server
if err := apiClient.ReportSystemInfo(cfg.AgentID, report); err != nil {
return fmt.Errorf("failed to report system info: %w", err)
}
return nil
}
// formatTimeSince formats a duration as "X time ago"
func formatTimeSince(t time.Time) string {
duration := time.Since(t)

View File

@@ -369,6 +369,52 @@ func (c *Client) ReportDependencies(agentID uuid.UUID, report DependencyReport)
return nil
}
// SystemInfoReport represents system information updates
type SystemInfoReport struct {
Timestamp time.Time `json:"timestamp"`
CPUModel string `json:"cpu_model,omitempty"`
CPUCores int `json:"cpu_cores,omitempty"`
CPUThreads int `json:"cpu_threads,omitempty"`
MemoryTotal uint64 `json:"memory_total,omitempty"`
DiskTotal uint64 `json:"disk_total,omitempty"`
DiskUsed uint64 `json:"disk_used,omitempty"`
IPAddress string `json:"ip_address,omitempty"`
Processes int `json:"processes,omitempty"`
Uptime string `json:"uptime,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// ReportSystemInfo sends updated system information to the server
func (c *Client) ReportSystemInfo(agentID uuid.UUID, report SystemInfoReport) error {
url := fmt.Sprintf("%s/api/v1/agents/%s/system-info", c.baseURL, agentID)
body, err := json.Marshal(report)
if err != nil {
return err
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+c.token)
resp, err := c.http.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
// Accept 200 OK or 404 Not Found (if endpoint doesn't exist yet)
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNotFound {
bodyBytes, _ := io.ReadAll(resp.Body)
return fmt.Errorf("failed to report system info: %s - %s", resp.Status, string(bodyBytes))
}
return nil
}
// DetectSystem returns basic system information (deprecated, use system.GetSystemInfo instead)
func DetectSystem() (osType, osVersion, osArch string) {
osType = runtime.GOOS

View File

@@ -6,6 +6,7 @@ import (
"os/exec"
"runtime"
"strings"
"time"
"github.com/aggregator-project/aggregator-agent/internal/client"
)
@@ -66,9 +67,17 @@ func (s *WingetScanner) Scan() ([]client.UpdateReportItem, error) {
fmt.Printf("Winget basic scan failed: %v\n", err)
}
// Method 3: Check if this is a known Winget issue and provide helpful error
// Method 3: Attempt automatic recovery for known issues
if isKnownWingetError(lastErr) {
return nil, fmt.Errorf("winget encountered a known issue (exit code %s). This may be due to Windows Update service or system configuration. Try running 'winget upgrade' manually to resolve", getExitCode(lastErr))
fmt.Printf("Attempting automatic winget recovery...\n")
if updates, err := s.attemptWingetRecovery(); err == nil {
fmt.Printf("Winget recovery successful, found %d updates\n", len(updates))
return updates, nil
} else {
fmt.Printf("Winget recovery failed: %v\n", err)
}
return nil, fmt.Errorf("winget encountered a known issue (exit code %s). This may be due to Windows Update service or system configuration. Automatic recovery was attempted but failed", getExitCode(lastErr))
}
return nil, lastErr
@@ -519,3 +528,135 @@ func (s *WingetScanner) GetInstalledPackages() ([]WingetPackage, error) {
return packages, nil
}
// attemptWingetRecovery tries to fix common winget issues automatically
func (s *WingetScanner) attemptWingetRecovery() ([]client.UpdateReportItem, error) {
fmt.Printf("Starting winget recovery process...\n")
// Recovery Method 1: Reset winget sources (common fix)
fmt.Printf("Attempting to reset winget sources...\n")
if err := s.resetWingetSources(); err == nil {
if updates, scanErr := s.scanWithJSON(); scanErr == nil {
fmt.Printf("Recovery successful after source reset\n")
return updates, nil
}
}
// Recovery Method 2: Update winget itself (silent)
fmt.Printf("Attempting to update winget itself...\n")
if err := s.updateWingetSilent(); err == nil {
// Wait a moment for winget to stabilize
time.Sleep(2 * time.Second)
if updates, scanErr := s.scanWithJSON(); scanErr == nil {
fmt.Printf("Recovery successful after winget update\n")
return updates, nil
}
}
// Recovery Method 3: Repair Windows App Installer (winget backend)
fmt.Printf("Attempting to repair Windows App Installer...\n")
if err := s.repairWindowsAppInstaller(); err == nil {
// Wait longer for system repairs
time.Sleep(5 * time.Second)
if updates, scanErr := s.scanWithJSON(); scanErr == nil {
fmt.Printf("Recovery successful after Windows App Installer repair\n")
return updates, nil
}
}
// Recovery Method 4: Force refresh with admin privileges
fmt.Printf("Attempting admin refresh...\n")
if updates, err := s.scanWithAdminPrivileges(); err == nil {
fmt.Printf("Recovery successful with admin privileges\n")
return updates, nil
}
// If all recovery attempts failed, return the original error
return nil, fmt.Errorf("all winget recovery attempts failed")
}
// resetWingetSources resets winget package sources
func (s *WingetScanner) resetWingetSources() error {
// Reset winget sources silently
cmd := exec.Command("winget", "source", "reset", "--force")
_, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("Failed to reset winget sources: %v\n", err)
return err
}
// Add default sources back
cmd = exec.Command("winget", "source", "add", "winget", "--accept-package-agreements", "--accept-source-agreements")
_, err = cmd.CombinedOutput()
if err != nil {
fmt.Printf("Failed to add winget source: %v\n", err)
return err
}
return nil
}
// updateWingetSilent updates winget itself silently
func (s *WingetScanner) updateWingetSilent() error {
// Update winget silently with no interaction
cmd := exec.Command("winget", "upgrade", "--id", "Microsoft.AppInstaller", "--silent", "--accept-package-agreements", "--accept-source-agreements")
_, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("Failed to update winget: %v\n", err)
return err
}
return nil
}
// repairWindowsAppInstaller attempts to repair the Windows App Installer
func (s *WingetScanner) repairWindowsAppInstaller() error {
// Try to repair using PowerShell
psCmd := `Get-AppxPackage -Name "Microsoft.DesktopAppInstaller" | Repair-AppxPackage -ForceUpdateFromAnyVersion`
cmd := exec.Command("powershell", "-ExecutionPolicy", "Bypass", "-Command", psCmd)
_, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("Failed to repair Windows App Installer: %v\n", err)
return err
}
return nil
}
// scanWithAdminPrivileges attempts to scan with elevated privileges if available
func (s *WingetScanner) scanWithAdminPrivileges() ([]client.UpdateReportItem, error) {
// Try running with elevated privileges using PowerShell
psCmd := `Start-Process winget -ArgumentList "list","--outdated","--accept-source-agreements" -Verb RunAs -Wait`
cmd := exec.Command("powershell", "-ExecutionPolicy", "Bypass", "-Command", psCmd)
// This will likely fail without actual admin privileges, but we try anyway
_, err := cmd.CombinedOutput()
if err != nil {
// Fallback to regular scan with different flags
return s.scanWithDifferentFlags()
}
// If admin scan succeeded, try to get the results
return s.scanWithBasicOutput()
}
// scanWithDifferentFlags tries alternative winget flags
func (s *WingetScanner) scanWithDifferentFlags() ([]client.UpdateReportItem, error) {
// Try different combination of flags
flagVariations := [][]string{
{"list", "--outdated", "--accept-source-agreements"},
{"list", "--outdated", "--include-unknown"},
{"list", "--outdated"},
}
for _, flags := range flagVariations {
cmd := exec.Command("winget", flags...)
output, err := cmd.CombinedOutput()
if err == nil {
// Try to parse the output
if updates, parseErr := s.parseWingetTextOutput(string(output)); parseErr == nil {
return updates, nil
}
}
}
return nil, fmt.Errorf("all flag variations failed")
}

View File

@@ -40,47 +40,96 @@ func getWindowsCPUInfo() (*CPUInfo, error) {
// Try using wmic for CPU information
if cmd, err := exec.LookPath("wmic"); err == nil {
// Get CPU name
// Get CPU name with better error handling
if data, err := exec.Command(cmd, "cpu", "get", "Name").Output(); err == nil {
lines := strings.Split(string(data), "\n")
output := string(data)
fmt.Printf("WMIC CPU Name output: '%s'\n", output) // Debug logging
lines := strings.Split(output, "\n")
for _, line := range lines {
if strings.TrimSpace(line) != "" && !strings.Contains(line, "Name") {
cpu.ModelName = strings.TrimSpace(line)
line = strings.TrimSpace(line)
if line != "" && !strings.Contains(line, "Name") {
cpu.ModelName = line
fmt.Printf("Found CPU model: '%s'\n", line) // Debug logging
break
}
}
} else {
fmt.Printf("Failed to get CPU name via wmic: %v\n", err)
}
// Get number of cores
if data, err := exec.Command(cmd, "cpu", "get", "NumberOfCores").Output(); err == nil {
lines := strings.Split(string(data), "\n")
output := string(data)
fmt.Printf("WMIC CPU Cores output: '%s'\n", output) // Debug logging
lines := strings.Split(output, "\n")
for _, line := range lines {
if strings.TrimSpace(line) != "" && !strings.Contains(line, "NumberOfCores") {
if cores, err := strconv.Atoi(strings.TrimSpace(line)); err == nil {
line = strings.TrimSpace(line)
if line != "" && !strings.Contains(line, "NumberOfCores") {
if cores, err := strconv.Atoi(line); err == nil {
cpu.Cores = cores
fmt.Printf("Found CPU cores: %d\n", cores) // Debug logging
}
break
}
}
} else {
fmt.Printf("Failed to get CPU cores via wmic: %v\n", err)
}
// Get number of logical processors (threads)
if data, err := exec.Command(cmd, "cpu", "get", "NumberOfLogicalProcessors").Output(); err == nil {
lines := strings.Split(string(data), "\n")
output := string(data)
fmt.Printf("WMIC CPU Threads output: '%s'\n", output) // Debug logging
lines := strings.Split(output, "\n")
for _, line := range lines {
if strings.TrimSpace(line) != "" && !strings.Contains(line, "NumberOfLogicalProcessors") {
if threads, err := strconv.Atoi(strings.TrimSpace(line)); err == nil {
line = strings.TrimSpace(line)
if line != "" && !strings.Contains(line, "NumberOfLogicalProcessors") {
if threads, err := strconv.Atoi(line); err == nil {
cpu.Threads = threads
fmt.Printf("Found CPU threads: %d\n", threads) // Debug logging
}
break
}
}
} else {
fmt.Printf("Failed to get CPU threads via wmic: %v\n", err)
}
// If we couldn't get threads, assume it's equal to cores
if cpu.Threads == 0 {
cpu.Threads = cpu.Cores
}
} else {
fmt.Printf("WMIC command not found, unable to get CPU info\n")
}
// Fallback to PowerShell if wmic failed
if cpu.ModelName == "" {
fmt.Printf("Attempting PowerShell fallback for CPU info...\n")
if psCmd, err := exec.LookPath("powershell"); err == nil {
// Get CPU info via PowerShell
if data, err := exec.Command(psCmd, "-Command", "Get-CimInstance -ClassName Win32_Processor | Select-Object -First 1 Name,NumberOfCores,NumberOfLogicalProcessors | ConvertTo-Json").Output(); err == nil {
fmt.Printf("PowerShell CPU output: '%s'\n", string(data))
// Try to parse JSON output (simplified)
output := string(data)
if strings.Contains(output, "Name") {
// Simple string extraction as fallback
lines := strings.Split(output, "\n")
for _, line := range lines {
if strings.Contains(line, "Name") && strings.Contains(line, ":") {
parts := strings.Split(line, ":")
if len(parts) >= 2 {
cpu.ModelName = strings.TrimSpace(strings.Trim(parts[1], " ,\""))
fmt.Printf("Found CPU via PowerShell: '%s'\n", cpu.ModelName)
break
}
}
}
}
} else {
fmt.Printf("PowerShell CPU info failed: %v\n", err)
}
}
}
return cpu, nil

View File

@@ -453,7 +453,56 @@ func (h *UpdateHandler) ReportDependencies(c *gin.Context) {
return
}
// Update the package status to pending_dependencies
// If there are NO dependencies, auto-approve and proceed directly to installation
// This prevents updates with zero dependencies from getting stuck in "pending_dependencies"
if len(req.Dependencies) == 0 {
// Get the update by package to retrieve its ID
update, err := h.updateQueries.GetUpdateByPackage(agentID, req.PackageType, req.PackageName)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get update details"})
return
}
// Record that dependencies were checked (empty array) in metadata
if err := h.updateQueries.SetPendingDependencies(agentID, req.PackageType, req.PackageName, req.Dependencies); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update package metadata"})
return
}
// Automatically create installation command since no dependencies need approval
command := &models.AgentCommand{
ID: uuid.New(),
AgentID: agentID,
CommandType: models.CommandTypeConfirmDependencies,
Params: map[string]interface{}{
"update_id": update.ID.String(),
"package_name": req.PackageName,
"package_type": req.PackageType,
"dependencies": []string{}, // Empty dependencies array
},
Status: models.CommandStatusPending,
CreatedAt: time.Now(),
}
if err := h.commandQueries.CreateCommand(command); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create installation command"})
return
}
// Update status to installing since no approval needed
if err := h.updateQueries.InstallUpdate(update.ID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update package status to installing"})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "no dependencies found - installation command created automatically",
"command_id": command.ID.String(),
})
return
}
// If dependencies EXIST, require manual approval by setting status to pending_dependencies
if err := h.updateQueries.SetPendingDependencies(agentID, req.PackageType, req.PackageName, req.Dependencies); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update package status"})
return

View File

@@ -222,7 +222,10 @@ func (q *UpdateQueries) SetCheckingDependencies(id uuid.UUID) error {
return err
}
// SetPendingDependencies marks an update as having dependencies that need approval
// SetPendingDependencies stores dependency information and sets status based on whether dependencies exist
// If dependencies array is empty, this function only updates metadata without changing status
// (the handler should auto-approve and proceed to installation in this case)
// If dependencies array has items, status is set to 'pending_dependencies' requiring manual approval
func (q *UpdateQueries) SetPendingDependencies(agentID uuid.UUID, packageType, packageName string, dependencies []string) error {
// Marshal dependencies to JSON for database storage
depsJSON, err := json.Marshal(dependencies)
@@ -230,6 +233,9 @@ func (q *UpdateQueries) SetPendingDependencies(agentID uuid.UUID, packageType, p
return fmt.Errorf("failed to marshal dependencies: %w", err)
}
// Note: When dependencies array is empty, the handler should bypass this status change
// and proceed directly to installation. This function still records the empty array
// in metadata for audit purposes before the handler transitions to 'installing'.
query := `
UPDATE current_package_state
SET status = 'pending_dependencies',