Skip to content

Commit

Permalink
contrib: Add a tool to analyze local Docker images
Browse files Browse the repository at this point in the history
  • Loading branch information
Quentin-M committed Nov 20, 2015
1 parent cfa960d commit 46f7645
Show file tree
Hide file tree
Showing 2 changed files with 270 additions and 0 deletions.
35 changes: 35 additions & 0 deletions contrib/analyze-local-images/README.md
Original file line number Diff line number Diff line change
@@ -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 <Docker Image ID>
```

Or, If you run Clair remotely (ie. boot2docker),

```
analyze-local-image -endpoint "http://<CLAIR-IP-ADDRESS>:6060" -my-address "<MY-IP-ADDRESS>" <Docker Image ID>
```

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.
235 changes: 235 additions & 0 deletions contrib/analyze-local-images/main.go
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit 46f7645

Please sign in to comment.