Complete RedFlag codebase with two major security audit implementations.
== A-1: Ed25519 Key Rotation Support ==
Server:
- SignCommand sets SignedAt timestamp and KeyID on every signature
- signing_keys database table (migration 020) for multi-key rotation
- InitializePrimaryKey registers active key at startup
- /api/v1/public-keys endpoint for rotation-aware agents
- SigningKeyQueries for key lifecycle management
Agent:
- Key-ID-aware verification via CheckKeyRotation
- FetchAndCacheAllActiveKeys for rotation pre-caching
- Cache metadata with TTL and staleness fallback
- SecurityLogger events for key rotation and command signing
== A-2: Replay Attack Fixes (F-1 through F-7) ==
F-5 CRITICAL - RetryCommand now signs via signAndCreateCommand
F-1 HIGH - v3 format: "{agent_id}:{cmd_id}:{type}:{hash}:{ts}"
F-7 HIGH - Migration 026: expires_at column with partial index
F-6 HIGH - GetPendingCommands/GetStuckCommands filter by expires_at
F-2 HIGH - Agent-side executedIDs dedup map with cleanup
F-4 HIGH - commandMaxAge reduced from 24h to 4h
F-3 CRITICAL - Old-format commands rejected after 48h via CreatedAt
Verification fixes: migration idempotency (ETHOS #4), log format
compliance (ETHOS #1), stale comments updated.
All 24 tests passing. Docker --no-cache build verified.
See docs/ for full audit reports and deviation log (DEV-001 to DEV-019).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
192 lines
5.4 KiB
Go
192 lines
5.4 KiB
Go
package orchestrator
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/Fimeg/RedFlag/aggregator-agent/internal/client"
|
|
"github.com/Fimeg/RedFlag/aggregator-agent/internal/system"
|
|
)
|
|
|
|
// SystemScanner scans system metrics (CPU, memory, processes, uptime)
|
|
type SystemScanner struct {
|
|
agentVersion string
|
|
}
|
|
|
|
// NewSystemScanner creates a new system scanner
|
|
func NewSystemScanner(agentVersion string) *SystemScanner {
|
|
return &SystemScanner{
|
|
agentVersion: agentVersion,
|
|
}
|
|
}
|
|
|
|
// IsAvailable always returns true since system scanning is always available
|
|
func (s *SystemScanner) IsAvailable() bool {
|
|
return true
|
|
}
|
|
|
|
// ScanSystem collects system information and returns proper system metrics
|
|
func (s *SystemScanner) ScanSystem() ([]SystemMetric, error) {
|
|
sysInfo, err := system.GetSystemInfo(s.agentVersion)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get system info: %w", err)
|
|
}
|
|
|
|
// Convert system info to proper SystemMetric format
|
|
var metrics []SystemMetric
|
|
|
|
// CPU info metric
|
|
cpuMetric := SystemMetric{
|
|
MetricName: "system-cpu",
|
|
MetricType: "cpu",
|
|
CurrentValue: fmt.Sprintf("%d cores, %d threads", sysInfo.CPUInfo.Cores, sysInfo.CPUInfo.Threads),
|
|
AvailableValue: sysInfo.CPUInfo.ModelName,
|
|
Severity: "low",
|
|
Metadata: map[string]interface{}{
|
|
"cpu_model": sysInfo.CPUInfo.ModelName,
|
|
"cpu_cores": fmt.Sprintf("%d", sysInfo.CPUInfo.Cores),
|
|
"cpu_threads": fmt.Sprintf("%d", sysInfo.CPUInfo.Threads),
|
|
},
|
|
}
|
|
metrics = append(metrics, cpuMetric)
|
|
|
|
// Memory info metric
|
|
memMetric := SystemMetric{
|
|
MetricName: "system-memory",
|
|
MetricType: "memory",
|
|
CurrentValue: fmt.Sprintf("%.1f%% used", sysInfo.MemoryInfo.UsedPercent),
|
|
AvailableValue: fmt.Sprintf("%d GB total", sysInfo.MemoryInfo.Total/(1024*1024*1024)),
|
|
Severity: determineMemorySeverity(sysInfo.MemoryInfo.UsedPercent),
|
|
Metadata: map[string]interface{}{
|
|
"memory_total": fmt.Sprintf("%d", sysInfo.MemoryInfo.Total),
|
|
"memory_used": fmt.Sprintf("%d", sysInfo.MemoryInfo.Used),
|
|
"memory_available": fmt.Sprintf("%d", sysInfo.MemoryInfo.Available),
|
|
"memory_used_percent": fmt.Sprintf("%.1f", sysInfo.MemoryInfo.UsedPercent),
|
|
},
|
|
}
|
|
metrics = append(metrics, memMetric)
|
|
|
|
// Process count metric
|
|
processMetric := SystemMetric{
|
|
MetricName: "system-processes",
|
|
MetricType: "processes",
|
|
CurrentValue: fmt.Sprintf("%d processes", sysInfo.RunningProcesses),
|
|
AvailableValue: "n/a",
|
|
Severity: "low",
|
|
Metadata: map[string]interface{}{
|
|
"process_count": fmt.Sprintf("%d", sysInfo.RunningProcesses),
|
|
},
|
|
}
|
|
metrics = append(metrics, processMetric)
|
|
|
|
// Uptime metric
|
|
uptimeMetric := SystemMetric{
|
|
MetricName: "system-uptime",
|
|
MetricType: "uptime",
|
|
CurrentValue: sysInfo.Uptime,
|
|
AvailableValue: "n/a",
|
|
Severity: "low",
|
|
Metadata: map[string]interface{}{
|
|
"uptime": sysInfo.Uptime,
|
|
},
|
|
}
|
|
metrics = append(metrics, uptimeMetric)
|
|
|
|
// Reboot required metric (if applicable)
|
|
if sysInfo.RebootRequired {
|
|
rebootMetric := SystemMetric{
|
|
MetricName: "system-reboot",
|
|
MetricType: "reboot",
|
|
CurrentValue: "required",
|
|
AvailableValue: "n/a",
|
|
Severity: "important",
|
|
Metadata: map[string]interface{}{
|
|
"reboot_required": "true",
|
|
"reboot_reason": sysInfo.RebootReason,
|
|
},
|
|
}
|
|
metrics = append(metrics, rebootMetric)
|
|
}
|
|
|
|
return metrics, nil
|
|
}
|
|
|
|
// Name returns the scanner name
|
|
func (s *SystemScanner) Name() string {
|
|
return "System Metrics Reporter"
|
|
}
|
|
|
|
// --- Legacy Compatibility Methods ---
|
|
|
|
// Scan collects system information and returns it as "updates" for reporting (LEGACY)
|
|
// This method is kept for backwards compatibility with the old Scanner interface
|
|
func (s *SystemScanner) Scan() ([]client.UpdateReportItem, error) {
|
|
metrics, err := s.ScanSystem()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert proper SystemMetric back to legacy UpdateReportItem format
|
|
var items []client.UpdateReportItem
|
|
|
|
for _, metric := range metrics {
|
|
item := client.UpdateReportItem{
|
|
PackageName: metric.MetricName,
|
|
CurrentVersion: metric.CurrentValue,
|
|
AvailableVersion: metric.AvailableValue,
|
|
PackageType: "system",
|
|
Severity: metric.Severity,
|
|
PackageDescription: fmt.Sprintf("System %s: %s", metric.MetricType, metric.MetricName),
|
|
Metadata: metric.Metadata,
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
|
|
return items, nil
|
|
}
|
|
|
|
// --- Typed Scanner Implementation ---
|
|
|
|
// GetType returns the scanner type
|
|
func (s *SystemScanner) GetType() ScannerType {
|
|
return ScannerTypeSystem
|
|
}
|
|
|
|
// ScanTyped returns typed results (new implementation)
|
|
func (s *SystemScanner) ScanTyped() (TypedScannerResult, error) {
|
|
startTime := time.Now()
|
|
|
|
metrics, err := s.ScanSystem()
|
|
if err != nil {
|
|
return TypedScannerResult{
|
|
ScannerName: s.Name(),
|
|
ScannerType: ScannerTypeSystem,
|
|
Error: err,
|
|
Status: "failed",
|
|
Duration: time.Since(startTime).Milliseconds(),
|
|
}, err
|
|
}
|
|
|
|
return TypedScannerResult{
|
|
ScannerName: s.Name(),
|
|
ScannerType: ScannerTypeSystem,
|
|
SystemData: metrics,
|
|
Status: "success",
|
|
Duration: time.Since(startTime).Milliseconds(),
|
|
}, nil
|
|
}
|
|
|
|
// determineMemorySeverity returns severity based on memory usage percentage
|
|
func determineMemorySeverity(usedPercent float64) string {
|
|
switch {
|
|
case usedPercent >= 95:
|
|
return "critical"
|
|
case usedPercent >= 90:
|
|
return "important"
|
|
case usedPercent >= 80:
|
|
return "moderate"
|
|
default:
|
|
return "low"
|
|
}
|
|
}
|