package orchestrator import ( "context" "fmt" "log" "sync" "time" "github.com/Fimeg/RedFlag/aggregator-agent/internal/circuitbreaker" "github.com/Fimeg/RedFlag/aggregator-agent/internal/client" "github.com/Fimeg/RedFlag/aggregator-agent/internal/event" "github.com/Fimeg/RedFlag/aggregator-agent/internal/models" ) // Scanner represents a generic update scanner type Scanner interface { // IsAvailable checks if the scanner is available on this system IsAvailable() bool // Scan performs the actual scanning and returns update items Scan() ([]client.UpdateReportItem, error) // Name returns the scanner name for logging Name() string } // ScannerConfig holds configuration for a single scanner type ScannerConfig struct { Scanner Scanner CircuitBreaker *circuitbreaker.CircuitBreaker Timeout time.Duration Enabled bool } // ScanResult holds the result of a scanner execution type ScanResult struct { ScannerName string Updates []client.UpdateReportItem Error error Duration time.Duration Status string // "success", "failed", "disabled", "unavailable", "skipped" } // Orchestrator manages and coordinates multiple scanners type Orchestrator struct { scanners map[string]*ScannerConfig eventBuffer *event.Buffer mu sync.RWMutex } // NewOrchestrator creates a new scanner orchestrator func NewOrchestrator() *Orchestrator { return &Orchestrator{ scanners: make(map[string]*ScannerConfig), } } // NewOrchestratorWithEvents creates a new scanner orchestrator with event buffering func NewOrchestratorWithEvents(buffer *event.Buffer) *Orchestrator { return &Orchestrator{ scanners: make(map[string]*ScannerConfig), eventBuffer: buffer, } } // RegisterScanner adds a scanner to the orchestrator func (o *Orchestrator) RegisterScanner(name string, scanner Scanner, cb *circuitbreaker.CircuitBreaker, timeout time.Duration, enabled bool) { o.mu.Lock() defer o.mu.Unlock() o.scanners[name] = &ScannerConfig{ Scanner: scanner, CircuitBreaker: cb, Timeout: timeout, Enabled: enabled, } } // ScanAll executes all registered scanners in parallel func (o *Orchestrator) ScanAll(ctx context.Context) ([]ScanResult, []client.UpdateReportItem) { o.mu.RLock() defer o.mu.RUnlock() var wg sync.WaitGroup resultsChan := make(chan ScanResult, len(o.scanners)) // Launch goroutine for each scanner for name, scannerConfig := range o.scanners { wg.Add(1) go func(name string, cfg *ScannerConfig) { defer wg.Done() result := o.executeScan(ctx, name, cfg) resultsChan <- result }(name, scannerConfig) } // Wait for all scanners to complete wg.Wait() close(resultsChan) // Collect results var results []ScanResult var allUpdates []client.UpdateReportItem for result := range resultsChan { results = append(results, result) if result.Error == nil && len(result.Updates) > 0 { allUpdates = append(allUpdates, result.Updates...) } } return results, allUpdates } // ScanSingle executes a single scanner by name func (o *Orchestrator) ScanSingle(ctx context.Context, scannerName string) (ScanResult, error) { o.mu.RLock() defer o.mu.RUnlock() cfg, exists := o.scanners[scannerName] if !exists { return ScanResult{ ScannerName: scannerName, Status: "failed", Error: fmt.Errorf("scanner not found: %s", scannerName), }, fmt.Errorf("scanner not found: %s", scannerName) } return o.executeScan(ctx, scannerName, cfg), nil } // executeScan runs a single scanner with circuit breaker and timeout protection func (o *Orchestrator) executeScan(ctx context.Context, name string, cfg *ScannerConfig) ScanResult { result := ScanResult{ ScannerName: name, Status: "failed", } startTime := time.Now() defer func() { result.Duration = time.Since(startTime) }() // Check if enabled if !cfg.Enabled { result.Status = "disabled" log.Printf("[%s] Scanner disabled via configuration", name) // Buffer disabled event if event buffer is available if o.eventBuffer != nil { event := &models.SystemEvent{ EventType: "agent_scan", EventSubtype: "skipped", Severity: "info", Component: "scanner", Message: fmt.Sprintf("Scanner %s is disabled via configuration", name), Metadata: map[string]interface{}{ "scanner_name": name, "status": "disabled", "reason": "configuration", }, CreatedAt: time.Now(), } if err := o.eventBuffer.BufferEvent(event); err != nil { log.Printf("Warning: Failed to buffer scanner disabled event: %v", err) } } return result } // Check if available if !cfg.Scanner.IsAvailable() { result.Status = "unavailable" log.Printf("[%s] Scanner not available on this system", name) // Buffer unavailable event if event buffer is available if o.eventBuffer != nil { event := &models.SystemEvent{ EventType: "agent_scan", EventSubtype: "skipped", Severity: "info", Component: "scanner", Message: fmt.Sprintf("Scanner %s is not available on this system", name), Metadata: map[string]interface{}{ "scanner_name": name, "status": "unavailable", "reason": "system_incompatible", }, CreatedAt: time.Now(), } if err := o.eventBuffer.BufferEvent(event); err != nil { log.Printf("Warning: Failed to buffer scanner unavailable event: %v", err) } } return result } // Execute with circuit breaker and timeout log.Printf("[%s] Starting scan...", name) var updates []client.UpdateReportItem err := cfg.CircuitBreaker.Call(func() error { // Create timeout context timeoutCtx, cancel := context.WithTimeout(ctx, cfg.Timeout) defer cancel() // Channel for scan result type scanResult struct { updates []client.UpdateReportItem err error } scanChan := make(chan scanResult, 1) // Run scan in goroutine go func() { u, e := cfg.Scanner.Scan() scanChan <- scanResult{updates: u, err: e} }() // Wait for scan or timeout select { case <-timeoutCtx.Done(): return fmt.Errorf("scan timeout after %v", cfg.Timeout) case res := <-scanChan: if res.err != nil { return res.err } updates = res.updates return nil } }) if err != nil { result.Error = err result.Status = "failed" log.Printf("[%s] Scan failed: %v", name, err) // Buffer event if event buffer is available if o.eventBuffer != nil { event := &models.SystemEvent{ EventType: "agent_scan", EventSubtype: "failed", Severity: "error", Component: "scanner", Message: fmt.Sprintf("Scanner %s failed: %v", name, err), Metadata: map[string]interface{}{ "scanner_name": name, "error_type": "scan_failed", "error_details": err.Error(), "duration_ms": result.Duration.Milliseconds(), }, CreatedAt: time.Now(), } if err := o.eventBuffer.BufferEvent(event); err != nil { log.Printf("Warning: Failed to buffer scanner failure event: %v", err) } } return result } result.Updates = updates result.Status = "success" log.Printf("[%s] Scan completed: found %d updates (took %v)", name, len(updates), result.Duration) // Buffer success event if event buffer is available if o.eventBuffer != nil { event := &models.SystemEvent{ EventType: "agent_scan", EventSubtype: "completed", Severity: "info", Component: "scanner", Message: fmt.Sprintf("Scanner %s completed successfully", name), Metadata: map[string]interface{}{ "scanner_name": name, "updates_found": len(updates), "duration_ms": result.Duration.Milliseconds(), "status": "success", }, CreatedAt: time.Now(), } if err := o.eventBuffer.BufferEvent(event); err != nil { log.Printf("Warning: Failed to buffer scanner success event: %v", err) } } return result } // GetScannerNames returns a list of all registered scanner names func (o *Orchestrator) GetScannerNames() []string { o.mu.RLock() defer o.mu.RUnlock() names := make([]string, 0, len(o.scanners)) for name := range o.scanners { names = append(names, name) } return names } // FormatScanSummary creates a human-readable summary of scan results func FormatScanSummary(results []ScanResult) (stdout string, stderr string, exitCode int) { var successResults []string var errorMessages []string totalUpdates := 0 for _, result := range results { switch result.Status { case "success": msg := fmt.Sprintf("%s: Found %d updates (%.2fs)", result.ScannerName, len(result.Updates), result.Duration.Seconds()) successResults = append(successResults, msg) totalUpdates += len(result.Updates) case "failed": msg := fmt.Sprintf("%s: %v", result.ScannerName, result.Error) errorMessages = append(errorMessages, msg) case "disabled": successResults = append(successResults, fmt.Sprintf("%s: Disabled", result.ScannerName)) case "unavailable": successResults = append(successResults, fmt.Sprintf("%s: Not available", result.ScannerName)) } } // Build stdout if len(successResults) > 0 { stdout = "Scan Results:\n" for _, msg := range successResults { stdout += fmt.Sprintf(" - %s\n", msg) } stdout += fmt.Sprintf("\nTotal Updates Found: %d\n", totalUpdates) } // Build stderr if len(errorMessages) > 0 { stderr = "Scan Errors:\n" for _, msg := range errorMessages { stderr += fmt.Sprintf(" - %s\n", msg) } } // Determine exit code if len(errorMessages) > 0 { exitCode = 1 } else { exitCode = 0 } return stdout, stderr, exitCode }