Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

contrib: Add a tool to analyze local Docker images #18

Merged
merged 1 commit into from
Nov 20, 2015
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}