diff --git a/contrib/analyze-local-images/main.go b/contrib/analyze-local-images/main.go index 53b16e5c..e1e2b4af 100644 --- a/contrib/analyze-local-images/main.go +++ b/contrib/analyze-local-images/main.go @@ -22,7 +22,6 @@ import ( "flag" "fmt" "io/ioutil" - "log" "net/http" "os" "os/exec" @@ -35,19 +34,20 @@ import ( const ( postLayerURI = "/v1/layers" - getLayerFeaturesURI = "/v1/layers/%s?features=true" + getLayerFeaturesURI = "/v1/layers/%s?vulnerabilities" httpPort = 9279 ) func main() { + // Parse command-line arguments. 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 { @@ -56,46 +56,41 @@ func main() { } imageName := flag.Args()[0] - // Save image + // 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) + fmt.Printf("- Could not save image: %s\n", err) + os.Exit(1) } - // Retrieve history + // Retrieve history. fmt.Println("Getting image's history") layerIDs, err := historyFromManifest(path) if err != nil { layerIDs, err = historyFromCommand(imageName) } if err != nil || len(layerIDs) == 0 { - log.Fatalf("- Could not get image's history: %s\n", err) + fmt.Printf("- Could not get image's history: %s\n", err) + os.Exit(1) } - // Setup a simple HTTP server if Clair is not local + // 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] - } + 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) + go listenHTTP(path, allowedHost) path = "http://" + *myAddress + ":" + strconv.Itoa(httpPort) time.Sleep(200 * time.Millisecond) } - // Analyze layers + // Analyze layers. fmt.Printf("Analyzing %d layers\n", len(layerIDs)) for i := 0; i < len(layerIDs); i++ { fmt.Printf("- Analyzing %s\n", layerIDs[i]) @@ -107,32 +102,61 @@ func main() { err = analyzeLayer(*endpoint, path+"/"+layerIDs[i]+"/layer.tar", layerIDs[i], "") } if err != nil { - log.Fatalf("- Could not analyze layer: %s\n", err) + fmt.Printf("- Could not analyze layer: %s\n", err) + os.Exit(1) } } - // Get vulnerabilities - fmt.Println("Getting image's features") - features, err := getFeatures(*endpoint, layerIDs[len(layerIDs)-1], *minimumPriority) + // Get vulnerabilities. + fmt.Println("Getting image's vulnerabilities") + layer, err := getLayer(*endpoint, layerIDs[len(layerIDs)-1]) if err != nil { - log.Fatalf("- Could not get features: %s\n", err) + fmt.Printf("- Could not get layer information: %s\n", err) + os.Exit(1) + } + + // Print report. + fmt.Printf("\n# Clair report for image %s (%s)\n", imageName, time.Now().UTC()) + + if len(layer.Features) == 0 { + fmt.Println("No feature has been detected on the image.") + fmt.Println("This usually means that the image isn't supported by Clair.") + os.Exit(0) } isSafe := true - for _, feature := range features { - if len(feature.Vulnerabilities) != 0 { + for _, feature := range layer.Features { + fmt.Printf("## Feature: %s %s (%s)\n", feature.Name, feature.Version, feature.Namespace) + + if len(feature.Vulnerabilities) > 0 { isSafe = false - fmt.Printf("- # Feature: %s %s %s\n", feature.Name, feature.Namespace, feature.Version) + + fmt.Printf(" - Added by: %s\n", feature.AddedBy) + for _, vulnerability := range feature.Vulnerabilities { - fmt.Printf("\t - Name: %s\n", vulnerability.Name) - fmt.Printf("\t - Priority: %s\n", vulnerability.Severity) - fmt.Printf("\t - Link: %s\n", vulnerability.Link) - fmt.Printf("\t - Description: %s\n", vulnerability.Description) + fmt.Printf("### (%s) %s\n", vulnerability.Severity, vulnerability.Name) + + if vulnerability.Description != "" { + fmt.Printf(" - Link: %s\n", vulnerability.Link) + } + + if vulnerability.Link != "" { + fmt.Printf(" - Description: %s\n", vulnerability.Description) + } + + if vulnerability.FixedBy != "" { + fmt.Printf(" - Fixed version: %s\n", vulnerability.FixedBy) + } + + if len(vulnerability.Metadata) > 0 { + fmt.Printf(" - Metadata: %+v\n", vulnerability.Metadata) + } } } } + if isSafe { - fmt.Println("Bravo, your image looks SAFE !") + fmt.Println("\nBravo, your image looks SAFE !") } } @@ -228,8 +252,37 @@ func historyFromCommand(imageName string) ([]string, error) { return layers, nil } +func listenHTTP(path, allowedHost string) { + fmt.Printf("Setting up HTTP server (allowing: %s)\n", allowedHost) + + restrictedFileServer := func(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) + } + + err := http.ListenAndServe(":"+strconv.Itoa(httpPort), restrictedFileServer(path, allowedHost)) + if err != nil { + fmt.Printf("- An error occurs with the HTTP server: %s\n", err) + os.Exit(1) + } +} + func analyzeLayer(endpoint, path, layerName, parentLayerName string) error { - payload := v1.LayerEnvelope{Layer: &v1.Layer{Name: layerName, Path: path, ParentName: parentLayerName, Format: "Docker"}} + payload := v1.LayerEnvelope{ + Layer: &v1.Layer{ + Name: layerName, + Path: path, + ParentName: parentLayerName, + Format: "Docker", + }, + } + jsonPayload, err := json.Marshal(payload) if err != nil { return err @@ -250,41 +303,31 @@ func analyzeLayer(endpoint, path, layerName, parentLayerName string) error { if response.StatusCode != 201 { body, _ := ioutil.ReadAll(response.Body) - return fmt.Errorf("Got response %d with message %s", response.StatusCode, string(body)) + return fmt.Errorf("- Got response %d with message %s", response.StatusCode, string(body)) } return nil } -func getFeatures(endpoint, layerID, minimumPriority string) ([]v1.Feature, error) { +func getLayer(endpoint, layerID string) (v1.Layer, error) { response, err := http.Get(endpoint + fmt.Sprintf(getLayerFeaturesURI, layerID)) if err != nil { - return []v1.Feature{}, err + return v1.Layer{}, err } defer response.Body.Close() if response.StatusCode != 200 { body, _ := ioutil.ReadAll(response.Body) - return []v1.Feature{}, fmt.Errorf("Got response %d with message %s", response.StatusCode, string(body)) + err := fmt.Errorf("- Got response %d with message %s", response.StatusCode, string(body)) + return v1.Layer{}, err } var apiResponse v1.LayerEnvelope if err = json.NewDecoder(response.Body).Decode(&apiResponse); err != nil { - return []v1.Feature{}, err + return v1.Layer{}, err } else if apiResponse.Error != nil { - return []v1.Feature{}, errors.New(apiResponse.Error.Message) + return v1.Layer{}, errors.New(apiResponse.Error.Message) } - return apiResponse.Layer.Features, 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) + return *apiResponse.Layer, nil }