contrib: Add a tool to analyze local Docker images

This commit is contained in:
Quentin Machu 2015-11-19 19:27:49 -05:00
parent cfa960d619
commit 46f7645a53
2 changed files with 270 additions and 0 deletions

View File

@ -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.

View File

@ -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)
}