feat: add config sync endpoint and security UI updates

- Add GET /api/v1/agents/:id/config endpoint for server configuration
- Agent fetches config during check-in and applies updates
- Add version tracking to prevent unnecessary config applications
- Clean separation: config sync independent of commands
- Fix agent UI subsystem settings to actually control agent behavior
- Update Security Health UI with frosted glass styling and tooltips
This commit is contained in:
Fimeg
2025-11-03 22:36:26 -05:00
parent eccc38d7c9
commit 38894f64d3
18 changed files with 944 additions and 87 deletions

View File

@@ -0,0 +1,266 @@
package orchestrator
import (
"context"
"fmt"
"os/exec"
"strings"
"time"
"github.com/Fimeg/RedFlag/aggregator-agent/internal/client"
"github.com/docker/docker/api/types/container"
dockerclient "github.com/docker/docker/client"
)
// DockerScanner scans for Docker image updates
type DockerScanner struct {
client *dockerclient.Client
registryClient *RegistryClient
}
// NewDockerScanner creates a new Docker scanner
func NewDockerScanner() (*DockerScanner, error) {
cli, err := dockerclient.NewClientWithOpts(dockerclient.FromEnv, dockerclient.WithAPIVersionNegotiation())
if err != nil {
return nil, err
}
return &DockerScanner{
client: cli,
registryClient: NewRegistryClient(),
}, nil
}
// IsAvailable checks if Docker is available on this system
func (s *DockerScanner) IsAvailable() bool {
_, err := exec.LookPath("docker")
if err != nil {
return false
}
// Try to ping Docker daemon
if s.client != nil {
_, err := s.client.Ping(context.Background())
return err == nil
}
return false
}
// ScanDocker scans for available Docker image updates and returns proper DockerImage data
func (s *DockerScanner) ScanDocker() ([]DockerImage, error) {
ctx := context.Background()
// List all containers
containers, err := s.client.ContainerList(ctx, container.ListOptions{All: true})
if err != nil {
return nil, fmt.Errorf("failed to list containers: %w", err)
}
var images []DockerImage
seenImages := make(map[string]bool)
for _, c := range containers {
imageName := c.Image
// Skip if we've already checked this image
if seenImages[imageName] {
continue
}
seenImages[imageName] = true
// Get current image details
imageInspect, _, err := s.client.ImageInspectWithRaw(ctx, imageName)
if err != nil {
continue
}
// Parse image name and tag
parts := strings.Split(imageName, ":")
baseImage := parts[0]
currentTag := "latest"
if len(parts) > 1 {
currentTag = parts[1]
}
// Check if update is available by comparing with registry
hasUpdate, remoteDigest := s.checkForUpdate(ctx, baseImage, currentTag, imageInspect.ID)
// Extract short digest for display (first 12 chars of sha256 hash)
localDigest := imageInspect.ID
localShortDigest := ""
if len(localDigest) > 7 {
parts := strings.SplitN(localDigest, ":", 2)
if len(parts) == 2 && len(parts[1]) >= 12 {
localShortDigest = parts[1][:12]
}
}
remoteShortDigest := ""
if len(remoteDigest) > 7 {
parts := strings.SplitN(remoteDigest, ":", 2)
if len(parts) == 2 && len(parts[1]) >= 12 {
remoteShortDigest = parts[1][:12]
}
}
// Determine severity based on update status
severity := "low"
if hasUpdate {
severity = "moderate"
}
// Extract image labels
labels := make(map[string]string)
if imageInspect.Config != nil {
labels = imageInspect.Config.Labels
}
// Get image size
sizeBytes := int64(0)
if len(imageInspect.RootFS.Layers) > 0 {
sizeBytes = imageInspect.Size
}
// Parse the creation time - imageInspect.Created is already a string
createdAt := imageInspect.Created
if createdAt == "" {
createdAt = time.Now().Format(time.RFC3339)
}
image := DockerImage{
ImageName: imageName,
ImageTag: currentTag,
ImageID: localShortDigest,
RepositorySource: baseImage,
SizeBytes: sizeBytes,
CreatedAt: createdAt,
HasUpdate: hasUpdate,
LatestImageID: remoteShortDigest,
Severity: severity,
Labels: labels,
Metadata: map[string]interface{}{
"container_id": c.ID[:12],
"container_names": c.Names,
"container_state": c.State,
"image_created": imageInspect.Created,
"local_full_digest": localDigest,
"remote_digest": remoteDigest,
},
}
images = append(images, image)
}
return images, nil
}
// Name returns the scanner name
func (s *DockerScanner) Name() string {
return "Docker Image Scanner"
}
// --- Legacy Compatibility Methods ---
// Scan scans for available Docker image updates (LEGACY)
// This method is kept for backwards compatibility with the old Scanner interface
func (s *DockerScanner) Scan() ([]client.UpdateReportItem, error) {
images, err := s.ScanDocker()
if err != nil {
return nil, err
}
// Convert proper DockerImage back to legacy UpdateReportItem format
var items []client.UpdateReportItem
for _, image := range images {
if image.HasUpdate { // Only include images that have updates
item := client.UpdateReportItem{
PackageType: "docker_image",
PackageName: image.ImageName,
PackageDescription: fmt.Sprintf("Docker Image: %s", image.ImageName),
CurrentVersion: image.ImageID,
AvailableVersion: image.LatestImageID,
Severity: image.Severity,
RepositorySource: image.RepositorySource,
Metadata: image.Metadata,
}
items = append(items, item)
}
}
return items, nil
}
// --- Typed Scanner Implementation ---
// GetType returns the scanner type
func (s *DockerScanner) GetType() ScannerType {
return ScannerTypeDocker
}
// ScanTyped returns typed results (new implementation)
func (s *DockerScanner) ScanTyped() (TypedScannerResult, error) {
startTime := time.Now()
images, err := s.ScanDocker()
if err != nil {
return TypedScannerResult{
ScannerName: s.Name(),
ScannerType: ScannerTypeDocker,
Error: err,
Status: "failed",
Duration: time.Since(startTime).Milliseconds(),
}, err
}
return TypedScannerResult{
ScannerName: s.Name(),
ScannerType: ScannerTypeDocker,
DockerData: images,
Status: "success",
Duration: time.Since(startTime).Milliseconds(),
}, nil
}
// checkForUpdate checks if a newer image version is available by comparing digests
// Returns (hasUpdate bool, remoteDigest string)
func (s *DockerScanner) checkForUpdate(ctx context.Context, imageName, tag, currentID string) (bool, string) {
// Get remote digest from registry
remoteDigest, err := s.registryClient.GetRemoteDigest(ctx, imageName, tag)
if err != nil {
// If we can't check the registry, log the error but don't report an update
fmt.Printf("Warning: Failed to check registry for %s:%s: %v\n", imageName, tag, err)
return false, ""
}
// Compare digests
hasUpdate := currentID != remoteDigest
return hasUpdate, remoteDigest
}
// Close closes the Docker client
func (s *DockerScanner) Close() error {
if s.client != nil {
return s.client.Close()
}
return nil
}
// --- Registry Client (simplified for this implementation) ---
// RegistryClient handles Docker registry API interactions
type RegistryClient struct{}
// NewRegistryClient creates a new registry client
func NewRegistryClient() *RegistryClient {
return &RegistryClient{}
}
// GetRemoteDigest gets the remote digest for an image from the registry
func (r *RegistryClient) GetRemoteDigest(ctx context.Context, imageName, tag string) (string, error) {
// This is a simplified implementation
// In a real implementation, you would query Docker Hub or the appropriate registry
// For now, return an empty string to indicate no remote digest available
return "", fmt.Errorf("registry client not implemented")
}

View File

@@ -2,7 +2,6 @@ package orchestrator
import (
"github.com/Fimeg/RedFlag/aggregator-agent/internal/client"
"github.com/Fimeg/RedFlag/aggregator-agent/internal/system"
)
// StorageMetric represents a single storage/disk metric

View File

@@ -45,16 +45,16 @@ func (s *StorageScanner) ScanStorage() ([]StorageMetric, error) {
Filesystem: disk.Filesystem,
Device: disk.Device,
DiskType: disk.DiskType,
TotalBytes: disk.Total,
UsedBytes: disk.Used,
AvailableBytes: disk.Available,
TotalBytes: int64(disk.Total),
UsedBytes: int64(disk.Used),
AvailableBytes: int64(disk.Available),
UsedPercent: disk.UsedPercent,
IsRoot: disk.IsRoot,
IsLargest: disk.IsLargest,
Severity: determineDiskSeverity(disk.UsedPercent),
Metadata: map[string]interface{}{
"agent_version": s.agentVersion,
"collected_at": sysInfo.Timestamp,
"collected_at": time.Now().Format(time.RFC3339),
},
}
metrics = append(metrics, metric)