From a4e7bb2d4d68135d57bc142a9ff3978dfa485357 Mon Sep 17 00:00:00 2001 From: Romain Cabassot Date: Fri, 5 Jul 2024 15:57:18 +0200 Subject: [PATCH] Convert Docker 24.1.x (and greater) image export from OCI format to legacy format. --- clair.go | 12 ++++++--- docker.go | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++---- scanner.go | 2 ++ server.go | 25 ++++++++++++++++- utils.go | 10 ++++++- 5 files changed, 118 insertions(+), 10 deletions(-) diff --git a/clair.go b/clair.go index 7b1a4ac..47851b1 100644 --- a/clair.go +++ b/clair.go @@ -7,7 +7,7 @@ import ( "io/ioutil" "net/http" - "github.com/coreos/clair/api/v1" + v1 "github.com/coreos/clair/api/v1" ) const ( @@ -28,15 +28,21 @@ type vulnerabilityInfo struct { // analyzeLayer tells Clair which layers to analyze func analyzeLayers(layerIds []string, clairURL string, scannerIP string) { + var layerPath string tmpPath := "http://" + scannerIP + ":" + httpPort for i := 0; i < len(layerIds); i++ { + if legacy { + layerPath = tmpPath + "/" + layerIds[i] + "/layer.tar" + } else { + layerPath = tmpPath + "/" + layerIds[i] + } logger.Infof("Analyzing %s", layerIds[i]) if i > 0 { - analyzeLayer(clairURL, tmpPath+"/"+layerIds[i]+"/layer.tar", layerIds[i], layerIds[i-1]) + analyzeLayer(clairURL, layerPath, layerIds[i], layerIds[i-1]) } else { - analyzeLayer(clairURL, tmpPath+"/"+layerIds[i]+"/layer.tar", layerIds[i], "") + analyzeLayer(clairURL, layerPath, layerIds[i], "") } } } diff --git a/docker.go b/docker.go index 61e94c5..3cfa68b 100644 --- a/docker.go +++ b/docker.go @@ -1,10 +1,16 @@ package main import ( + "bytes" "context" "encoding/json" + "errors" + "fmt" "io" + "log" "os" + "os/exec" + "strconv" "strings" "github.com/docker/docker/client" @@ -16,20 +22,56 @@ type manifestJSON struct { Layers []string } +type newManifestJSON struct { + Layers []newManifestJSONDigest `json:"layers"` +} + +type newManifestJSONDigest struct { + Digest string `json:"digest"` +} + // saveDockerImage saves Docker image to temorary folder func saveDockerImage(imageName string, tmpPath string) { docker := createDockerClient() - imageReader, err := docker.ImageSave(context.Background(), []string{imageName}) + version, err := docker.ServerVersion(context.Background()) if err != nil { - logger.Fatalf("Could not save Docker image [%s]: %v", imageName, err) + logger.Fatalf("Could not find Docker version: %v", err) } + majorVersion, err := strconv.Atoi(strings.Split(version.Version, ".")[0]) + if err != nil { + logger.Fatalf("Error while parsing Docker version '%s': %v", version.Version, err) + } + if majorVersion < 25 { + legacy = true + imageReader, err := docker.ImageSave(context.Background(), []string{imageName}) + if err != nil { + logger.Fatalf("Could not save Docker image [%s]: %v", imageName, err) + } + defer imageReader.Close() - defer imageReader.Close() + if err = untar(imageReader, tmpPath); err != nil { + logger.Fatalf("Could not save Docker image: could not untar [%s]: %v", imageName, err) + } + } else { + updateDockerImage(imageName, tmpPath) + } +} + +func updateDockerImage(imageName string, tmpPath string) { + logger.Infof("Converting Docker image '%s' in '%s' to legacy format...", imageName, tmpPath) + cmd := exec.Command(getEnv("SKOPEO_BIN_PATH", "skopeo"), "copy", "--format", "v2s2", fmt.Sprintf("docker-daemon:%s", imageName), fmt.Sprintf("dir:%s", tmpPath)) + var outb, errb bytes.Buffer + cmd.Stdout = &outb + cmd.Stderr = &errb - if err = untar(imageReader, tmpPath); err != nil { - logger.Fatalf("Could not save Docker image: could not untar [%s]: %v", imageName, err) + if errors.Is(cmd.Err, exec.ErrDot) { + cmd.Err = nil + } + if err := cmd.Run(); err != nil { + log.Fatalf("Error running skopeo: %s %s", err, errb.String()) } + updateLegacyManifestFile(tmpPath) } func createDockerClient() client.APIClient { @@ -75,3 +117,30 @@ func parseAndValidateManifestFile(manifestFile io.Reader) []manifestJSON { } return manifest } + +// readManifestFile reads the local manifest.json +func updateLegacyManifestFile(path string) { + manifestFile := path + "/manifest.json" + mf, err := os.Open(manifestFile) + if err != nil { + logger.Fatalf("Could not read Docker image layers: could not open [%s]: %v", manifestFile, err) + } + defer mf.Close() + + var manifest newManifestJSON + + if err := json.NewDecoder(mf).Decode(&manifest); err != nil { + logger.Fatalf("Could not read Docker image layers: manifest.json is not json: %v", err) + } + mf.Close() + var legacyManifest []manifestJSON = []manifestJSON{ + { + Layers: make([]string, len(manifest.Layers)), + }, + } + for i, v := range manifest.Layers { + legacyManifest[0].Layers[i] = strings.TrimPrefix(v.Digest, "sha256:") + } + manifestFileContent, _ := json.MarshalIndent(legacyManifest, "", " ") + os.WriteFile(manifestFile, manifestFileContent, 0644) +} diff --git a/scanner.go b/scanner.go index c16ec8b..f4d012c 100644 --- a/scanner.go +++ b/scanner.go @@ -13,6 +13,8 @@ type vulnerabilitiesWhitelist struct { const tmpPrefix = "clair-scanner-" +var legacy = false + type scannerConfig struct { imageName string whitelist vulnerabilitiesWhitelist diff --git a/server.go b/server.go index 6426794..a46694e 100644 --- a/server.go +++ b/server.go @@ -9,10 +9,20 @@ const ( httpPort = "9279" ) +type StatusRespWr struct { + http.ResponseWriter // We embed http.ResponseWriter + status int +} + +func (w *StatusRespWr) WriteHeader(status int) { + w.status = status // Store the status for our own use + w.ResponseWriter.WriteHeader(status) +} + // httpFileServer servers files from a specified folder // TODO if port can't be opened is not handled func httpFileServer(path string) *http.Server { - server := &http.Server{Addr: ":" + httpPort} + server := &http.Server{Addr: ":" + httpPort, Handler: logRequest(http.DefaultServeMux)} http.Handle("/", http.FileServer(http.Dir(path))) go func() { server.ListenAndServe() @@ -21,3 +31,16 @@ func httpFileServer(path string) *http.Server { logger.Infof("Server listening on port %s", httpPort) return server } + +func logRequest(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + logger.Debugf("%s %s %s\n", r.RemoteAddr, r.Method, r.URL) + srw := &StatusRespWr{ResponseWriter: w} + handler.ServeHTTP(srw, r) + if srw.status >= 400 { // 400+ codes are the error codes + logger.Debugf("Error status code: %d when serving path: %s", + srw.status, r.RequestURI) + } + + }) +} diff --git a/utils.go b/utils.go index c5df968..9fc4284 100644 --- a/utils.go +++ b/utils.go @@ -66,7 +66,7 @@ func untar(imageReader io.ReadCloser, target string) error { } path := filepath.Join(target, header.Name) - if !strings.HasPrefix(path, filepath.Clean(target) + string(os.PathSeparator)) { + if !strings.HasPrefix(path, filepath.Clean(target)+string(os.PathSeparator)) { return fmt.Errorf("%s: illegal file path", header.Name) } info := header.FileInfo() @@ -112,3 +112,11 @@ func validateThreshold(threshold string) { } logger.Fatalf("Invalid CVE severity threshold %s given", threshold) } + +func getEnv(key, defaultValue string) string { + value := os.Getenv(key) + if len(value) == 0 { + return defaultValue + } + return value +}