- 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
266 lines
7.1 KiB
Go
266 lines
7.1 KiB
Go
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")
|
|
} |