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") }