358 lines
9.4 KiB
Go
358 lines
9.4 KiB
Go
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
|
|
}
|