- Update go.mod files to use github.com/Fimeg/RedFlag module path - Fix all import statements across server and agent code - Resolves build errors when cloning from GitHub - Utils package (version comparison) is actually needed and working
163 lines
4.6 KiB
Go
163 lines
4.6 KiB
Go
package scanner
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"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
|
|
}
|
|
|
|
// 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
|
|
}
|