From 46f7645a53772310cbbacab8bd6aba8ae91fe63e Mon Sep 17 00:00:00 2001 From: Quentin Machu Date: Thu, 19 Nov 2015 19:27:49 -0500 Subject: [PATCH] contrib: Add a tool to analyze local Docker images --- contrib/analyze-local-images/README.md | 35 ++++ contrib/analyze-local-images/main.go | 235 +++++++++++++++++++++++++ 2 files changed, 270 insertions(+) create mode 100644 contrib/analyze-local-images/README.md create mode 100644 contrib/analyze-local-images/main.go diff --git a/contrib/analyze-local-images/README.md b/contrib/analyze-local-images/README.md new file mode 100644 index 00000000..e84b0a10 --- /dev/null +++ b/contrib/analyze-local-images/README.md @@ -0,0 +1,35 @@ +# Analyze local images + +This is a basic tool that allow you to analyze your local Docker images with Clair. +It is intended to let everyone discover Clair and offer awareness around containers' security. +There are absolutely no guarantees and it only uses a minimal subset of Clair's features. + +## Install + +You need to install this tool: + + go install github.com/coreos/clair/contrib/analyze-local-image + +You also need a working Clair instance, the bare minimal setup is to run Clair in a Docker instance without much configuration: + + docker run -it -p 6060:6060 -p 6061:6061 quay.io/coreos/clair --db-path=/db/bolt + +You will need to let it do its initial vulnerability update, which may take some time. + +# Usage + +If you are running Clair locally (ie. compiled or local Docker), + +``` +analyze-local-image +``` + +Or, If you run Clair remotely (ie. boot2docker), + +``` +analyze-local-image -endpoint "http://:6060" -my-address "" +``` + +Clair needs access to the image files. If you run Clair locally, it will directly find them in the filesystem. If you run Clair remotely, this tool will run a small HTTP server to let Clair downloading them. It listens on the port 9279 and allows a single host: Clair's IP address, extracted from the `-endpoint` parameter. The `my-address` parameters defines the IP address of the HTTP server that Clair will use to download the images. With boot2docker, these parameters would be `-endpoint "http://192.168.99.100:6060" -my-address "192.168.99.1"`. + +As it runs an HTTP server and not an HTTP**S** one, be sure to **not** expose sensitive data and container images. diff --git a/contrib/analyze-local-images/main.go b/contrib/analyze-local-images/main.go new file mode 100644 index 00000000..887e2440 --- /dev/null +++ b/contrib/analyze-local-images/main.go @@ -0,0 +1,235 @@ +package main + +import ( + "bufio" + "bytes" + "encoding/json" + "errors" + "flag" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "os/exec" + "strconv" + "strings" + "time" +) + +const ( + postLayerURI = "/v1/layers" + getLayerVulnerabilitiesURI = "/v1/layers/%s/vulnerabilities?minimumPriority=%s" + httpPort = 9279 +) + +type APIVulnerabilitiesResponse struct { + Vulnerabilities []APIVulnerability +} + +type APIVulnerability struct { + ID, Link, Priority, Description string +} + +func main() { + endpoint := flag.String("endpoint", "http://127.0.0.1:6060", "Address to Clair API") + myAddress := flag.String("my-address", "127.0.0.1", "Address from the point of view of Clair") + minimumPriority := flag.String("minimum-priority", "Low", "Minimum vulnerability vulnerability to show") + + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage: %s [options] image-id\n\nOptions:\n", os.Args[0]) + flag.PrintDefaults() + } + flag.Parse() + + if len(flag.Args()) != 1 { + flag.Usage() + os.Exit(1) + } + imageName := flag.Args()[0] + + // Save image + fmt.Printf("Saving %s\n", imageName) + path, err := save(imageName) + defer os.RemoveAll(path) + if err != nil { + log.Fatalf("- Could not save image: %s\n", err) + } + + // Retrieve history + fmt.Println("Getting image's history") + layerIDs, err := history(imageName) + if err != nil || len(layerIDs) == 0 { + log.Fatalf("- Could not get image's history: %s\n", err) + } + + // Setup a simple HTTP server if Clair is not local + if !strings.Contains(*endpoint, "127.0.0.1") && !strings.Contains(*endpoint, "localhost") { + go func(path string) { + allowedHost := strings.TrimPrefix(*endpoint, "http://") + portIndex := strings.Index(allowedHost, ":") + if portIndex >= 0 { + allowedHost = allowedHost[:portIndex] + } + + fmt.Printf("Setting up HTTP server (allowing: %s)\n", allowedHost) + + err := http.ListenAndServe(":"+strconv.Itoa(httpPort), restrictedFileServer(path, allowedHost)) + if err != nil { + log.Fatalf("- An error occurs with the HTTP Server: %s\n", err) + } + }(path) + + path = "http://" + *myAddress + ":" + strconv.Itoa(httpPort) + time.Sleep(200 * time.Millisecond) + } + + // Analyze layers + fmt.Printf("Analyzing %d layers\n", len(layerIDs)) + for i := 0; i < len(layerIDs); i++ { + fmt.Printf("- Analyzing %s\n", layerIDs[i]) + + var err error + if i > 0 { + err = analyzeLayer(*endpoint, path+"/"+layerIDs[i]+"/layer.tar", layerIDs[i], layerIDs[i-1]) + } else { + err = analyzeLayer(*endpoint, path+"/"+layerIDs[i]+"/layer.tar", layerIDs[i], "") + } + if err != nil { + log.Fatalf("- Could not analyze layer: %s\n", err) + } + } + + // Get vulnerabilities + fmt.Println("Getting image's vulnerabilities") + vulnerabilities, err := getVulnerabilities(*endpoint, layerIDs[len(layerIDs)-1], *minimumPriority) + if err != nil { + log.Fatalf("- Could not get vulnerabilities: %s\n", err) + } + if len(vulnerabilities) == 0 { + fmt.Println("Bravo, your image looks SAFE !") + } + for _, vulnerability := range vulnerabilities { + fmt.Printf("- # %s\n", vulnerability.ID) + fmt.Printf(" - Priority: %s\n", vulnerability.Priority) + fmt.Printf(" - Link: %s\n", vulnerability.Link) + fmt.Printf(" - Description: %s\n", vulnerability.Description) + } +} + +func save(imageName string) (string, error) { + path, err := ioutil.TempDir("", "analyze-local-image-") + if err != nil { + return "", err + } + + var stderr bytes.Buffer + save := exec.Command("docker", "save", imageName) + save.Stderr = &stderr + extract := exec.Command("tar", "xzf", "-", "-C"+path) + extract.Stderr = &stderr + pipe, err := extract.StdinPipe() + if err != nil { + return "", err + } + save.Stdout = pipe + + err = extract.Start() + if err != nil { + return "", errors.New(stderr.String()) + } + err = save.Run() + if err != nil { + return "", errors.New(stderr.String()) + } + + return path, nil +} + +func history(imageName string) ([]string, error) { + var stderr bytes.Buffer + cmd := exec.Command("docker", "history", "-q", "--no-trunc", imageName) + cmd.Stderr = &stderr + stdout, err := cmd.StdoutPipe() + if err != nil { + return []string{}, err + } + + err = cmd.Start() + if err != nil { + return []string{}, errors.New(stderr.String()) + } + + var layers []string + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + layers = append(layers, scanner.Text()) + } + + for i := len(layers)/2 - 1; i >= 0; i-- { + opp := len(layers) - 1 - i + layers[i], layers[opp] = layers[opp], layers[i] + } + + return layers, nil +} + +func analyzeLayer(endpoint, path, layerID, parentLayerID string) error { + payload := struct{ ID, Path, ParentID string }{ID: layerID, Path: path, ParentID: parentLayerID} + jsonPayload, err := json.Marshal(payload) + if err != nil { + return err + } + + request, err := http.NewRequest("POST", endpoint+postLayerURI, bytes.NewBuffer(jsonPayload)) + if err != nil { + return err + } + request.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + response, err := client.Do(request) + if err != nil { + return err + } + defer response.Body.Close() + + if response.StatusCode != 201 { + body, _ := ioutil.ReadAll(response.Body) + return fmt.Errorf("Got response %d with message %s", response.StatusCode, string(body)) + } + + return nil +} + +func getVulnerabilities(endpoint, layerID, minimumPriority string) ([]APIVulnerability, error) { + response, err := http.Get(endpoint + fmt.Sprintf(getLayerVulnerabilitiesURI, layerID, minimumPriority)) + if err != nil { + return []APIVulnerability{}, err + } + defer response.Body.Close() + + if response.StatusCode != 200 { + body, _ := ioutil.ReadAll(response.Body) + return []APIVulnerability{}, fmt.Errorf("Got response %d with message %s", response.StatusCode, string(body)) + } + + var apiResponse APIVulnerabilitiesResponse + err = json.NewDecoder(response.Body).Decode(&apiResponse) + if err != nil { + return []APIVulnerability{}, err + } + + return apiResponse.Vulnerabilities, nil +} + +func restrictedFileServer(path, allowedHost string) http.Handler { + fc := func(w http.ResponseWriter, r *http.Request) { + if r.Host == allowedHost { + http.FileServer(http.Dir(path)).ServeHTTP(w, r) + return + } + w.WriteHeader(403) + } + return http.HandlerFunc(fc) +}