-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #18 from Quentin-M/local-analysis
contrib: Add a tool to analyze local Docker images
- Loading branch information
Showing
2 changed files
with
270 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |