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:
118
README.md
118
README.md
@@ -52,33 +52,20 @@ A self-hosted, cross-platform update management platform built with:
|
||||
|
||||
## Screenshots
|
||||
|
||||
### Main Dashboard
|
||||

|
||||
Overview showing agent status, system metrics, and update statistics
|
||||
| Overview | Updates Management | Agent List |
|
||||
|----------|-------------------|------------|
|
||||
|  |  |  |
|
||||
| System overview with metrics | Update approval with dependency workflow | Cross-platform agent management |
|
||||
|
||||
### Updates Management
|
||||

|
||||
Comprehensive update listing with filtering, approval, and dependency confirmation
|
||||
| Linux Agent Details | Windows Agent Details | History & Audit |
|
||||
|-------------------|---------------------|----------------|
|
||||
|  |  |  |
|
||||
| Linux system specs and updates | Windows Updates and Winget support | Complete audit trail of activities |
|
||||
|
||||
### Agent Details
|
||||

|
||||
Detailed agent information including system specs, last check-in, and individual update management
|
||||
|
||||
### Windows Agent Support
|
||||

|
||||
Cross-platform support for Windows Updates and Winget package management
|
||||
|
||||
### History & Audit Trail
|
||||

|
||||
Complete audit trail of all update activities and command execution
|
||||
|
||||
### Live Operations
|
||||

|
||||
Real-time view of update operations with success/failure tracking
|
||||
|
||||
### Docker Container Management
|
||||

|
||||
Docker-specific interface for container image updates and management
|
||||
| Live Operations | Docker Management |
|
||||
|-----------------|------------------|
|
||||
|  |  |
|
||||
| Real-time operation tracking | Container image update management |
|
||||
|
||||
## For Developers
|
||||
|
||||
@@ -104,9 +91,9 @@ This repository contains:
|
||||
┌────┴────┬────────┐
|
||||
│ │ │
|
||||
┌───▼──┐ ┌──▼──┐ ┌──▼───┐
|
||||
│Linux │ │Linux│ │Linux │
|
||||
│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 |
BIN
Screenshots/RedFlag Agent List.png
Normal file
BIN
Screenshots/RedFlag Agent List.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user