Files
Redflag/aggregator-agent/internal/scanner/docker.go
Fimeg 55b7d03010 Session 4 complete - RedFlag update management platform
🚩 Private development - version retention only

 Complete web dashboard (React + TypeScript + TailwindCSS)
 Production-ready server backend (Go + Gin + PostgreSQL)
 Linux agent with APT + Docker scanning + local CLI tools
 JWT authentication and REST API
 Update discovery and approval workflow

🚧 Status: Alpha software - active development
📦 Purpose: Version retention during development
⚠️  Not for public use or deployment
2025-10-13 16:46:31 -04:00

163 lines
4.6 KiB
Go

package scanner
import (
"context"
"fmt"
"os/exec"
"strings"
"github.com/aggregator-project/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
}
// Scan scans for available Docker image updates
func (s *DockerScanner) Scan() ([]client.UpdateReportItem, 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 updates []client.UpdateReportItem
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)
if hasUpdate {
// Extract short digest for display (first 12 chars of sha256 hash)
localDigest := imageInspect.ID
remoteShortDigest := "unknown"
if len(remoteDigest) > 7 {
// Format: sha256:abcd... -> take first 12 chars of hash
parts := strings.SplitN(remoteDigest, ":", 2)
if len(parts) == 2 && len(parts[1]) >= 12 {
remoteShortDigest = parts[1][:12]
}
}
update := client.UpdateReportItem{
PackageType: "docker_image",
PackageName: imageName,
PackageDescription: fmt.Sprintf("Container: %s", strings.Join(c.Names, ", ")),
CurrentVersion: localDigest[:12], // Short hash
AvailableVersion: remoteShortDigest,
Severity: "moderate",
RepositorySource: baseImage,
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,
},
}
updates = append(updates, update)
}
}
return updates, nil
}
// checkForUpdate checks if a newer image version is available by comparing digests
// Returns (hasUpdate bool, remoteDigest string)
//
// This implementation:
// 1. Queries Docker Registry HTTP API v2 for remote manifest
// 2. Compares image digests (sha256 hashes) between local and remote
// 3. Handles authentication for Docker Hub (anonymous pull)
// 4. Caches registry responses (5 min TTL) to respect rate limits
// 5. Returns both the update status and remote digest for metadata
//
// Note: This compares exact digests. If local digest != remote digest, an update exists.
// This works for all tags including "latest", version tags, etc.
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
// This prevents false positives when registry is down or rate-limited
fmt.Printf("Warning: Failed to check registry for %s:%s: %v\n", imageName, tag, err)
return false, ""
}
// Compare digests
// Local Docker image ID format: sha256:abc123...
// Remote digest format: sha256:def456...
// If they differ, an update is available
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
}