clair/contrib/analyze-local-images/main.go

464 lines
12 KiB
Go

// Copyright 2015 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"flag"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"os/exec"
"os/signal"
"sort"
"strconv"
"strings"
"time"
"github.com/coreos/clair/api/v1"
"github.com/coreos/clair/utils/types"
"github.com/fatih/color"
"github.com/kr/text"
)
const (
postLayerURI = "/v1/layers"
getLayerFeaturesURI = "/v1/layers/%s?vulnerabilities"
httpPort = 9279
)
var (
flagEndpoint = flag.String("endpoint", "http://127.0.0.1:6060", "Address to Clair API")
flagMyAddress = flag.String("my-address", "127.0.0.1", "Address from the point of view of Clair")
flagMinimumSeverity = flag.String("minimum-severity", "Negligible", "Minimum severity of vulnerabilities to show (Unknown, Negligible, Low, Medium, High, Critical, Defcon1)")
flagColorMode = flag.String("color", "auto", "Colorize the output (always, auto, never)")
)
type vulnerabilityInfo struct {
vulnerability v1.Vulnerability
feature v1.Feature
severity types.Priority
}
type By func(v1, v2 vulnerabilityInfo) bool
func (by By) Sort(vulnerabilities []vulnerabilityInfo) {
ps := &sorter{
vulnerabilities: vulnerabilities,
by: by,
}
sort.Sort(ps)
}
type sorter struct {
vulnerabilities []vulnerabilityInfo
by func(v1, v2 vulnerabilityInfo) bool
}
func (s *sorter) Len() int {
return len(s.vulnerabilities)
}
func (s *sorter) Swap(i, j int) {
s.vulnerabilities[i], s.vulnerabilities[j] = s.vulnerabilities[j], s.vulnerabilities[i]
}
func (s *sorter) Less(i, j int) bool {
return s.by(s.vulnerabilities[i], s.vulnerabilities[j])
}
func main() {
os.Exit(intMain())
}
func intMain() int {
// Parse command-line arguments.
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()
return 1
}
imageName := flag.Args()[0]
minSeverity := types.Priority(*flagMinimumSeverity)
if !minSeverity.IsValid() {
flag.Usage()
return 1
}
if *flagColorMode == "never" {
color.NoColor = true
} else if *flagColorMode == "always" {
color.NoColor = false
}
// Create a temporary folder.
tmpPath, err := ioutil.TempDir("", "analyze-local-image-")
if err != nil {
log.Fatalf("Could not create temporary folder: %s", err)
}
defer os.RemoveAll(tmpPath)
// Intercept SIGINT / SIGKILl signals.
interrupt := make(chan os.Signal)
signal.Notify(interrupt, os.Interrupt, os.Kill)
// Analyze the image.
analyzeCh := make(chan error, 1)
go func() {
analyzeCh <- AnalyzeLocalImage(imageName, minSeverity, *flagEndpoint, *flagMyAddress, tmpPath)
}()
select {
case <-interrupt:
return 130
case err := <-analyzeCh:
if err != nil {
log.Print(err)
return 1
}
}
return 0
}
func AnalyzeLocalImage(imageName string, minSeverity types.Priority, endpoint, myAddress, tmpPath string) error {
// Save image.
log.Printf("Saving %s to local disk (this may take some time)", imageName)
err := save(imageName, tmpPath)
if err != nil {
return fmt.Errorf("Could not save image: %s", err)
}
// Retrieve history.
log.Println("Retrieving image history")
layerIDs, err := historyFromManifest(tmpPath)
if err != nil {
layerIDs, err = historyFromCommand(imageName)
}
if err != nil || len(layerIDs) == 0 {
return fmt.Errorf("Could not get image's history: %s", err)
}
// Setup a simple HTTP server if Clair is not local.
if !strings.Contains(endpoint, "127.0.0.1") && !strings.Contains(endpoint, "localhost") {
allowedHost := strings.TrimPrefix(endpoint, "http://")
portIndex := strings.Index(allowedHost, ":")
if portIndex >= 0 {
allowedHost = allowedHost[:portIndex]
}
log.Printf("Setting up HTTP server (allowing: %s)\n", allowedHost)
ch := make(chan error)
go listenHTTP(tmpPath, allowedHost, ch)
select {
case err := <-ch:
return fmt.Errorf("An error occured when starting HTTP server: %s", err)
case <-time.After(100 * time.Millisecond):
break
}
tmpPath = "http://" + myAddress + ":" + strconv.Itoa(httpPort)
}
// Analyze layers.
log.Printf("Analyzing %d layers... \n", len(layerIDs))
for i := 0; i < len(layerIDs); i++ {
log.Printf("Analyzing %s\n", layerIDs[i])
if i > 0 {
err = analyzeLayer(endpoint, tmpPath+"/"+layerIDs[i]+"/layer.tar", layerIDs[i], layerIDs[i-1])
} else {
err = analyzeLayer(endpoint, tmpPath+"/"+layerIDs[i]+"/layer.tar", layerIDs[i], "")
}
if err != nil {
return fmt.Errorf("Could not analyze layer: %s", err)
}
}
// Get vulnerabilities.
log.Println("Retrieving image's vulnerabilities")
layer, err := getLayer(endpoint, layerIDs[len(layerIDs)-1])
if err != nil {
return fmt.Errorf("Could not get layer information: %s", err)
}
// Print report.
fmt.Printf("Clair report for image %s (%s)\n", imageName, time.Now().UTC())
if len(layer.Features) == 0 {
fmt.Printf("%s No features have been detected in the image. This usually means that the image isn't supported by Clair.\n", color.YellowString("NOTE:"))
return nil
}
isSafe := true
hasVisibleVulnerabilities := false
var vulnerabilities = make([]vulnerabilityInfo, 0)
for _, feature := range layer.Features {
if len(feature.Vulnerabilities) > 0 {
for _, vulnerability := range feature.Vulnerabilities {
severity := types.Priority(vulnerability.Severity)
isSafe = false
if minSeverity.Compare(severity) > 0 {
continue
}
hasVisibleVulnerabilities = true
vulnerabilities = append(vulnerabilities, vulnerabilityInfo{vulnerability, feature, severity})
}
}
}
// Sort vulnerabilitiy by severity.
priority := func(v1, v2 vulnerabilityInfo) bool {
return v1.severity.Compare(v2.severity) >= 0
}
By(priority).Sort(vulnerabilities)
for _, vulnerabilityInfo := range vulnerabilities {
vulnerability := vulnerabilityInfo.vulnerability
feature := vulnerabilityInfo.feature
severity := vulnerabilityInfo.severity
fmt.Printf("%s (%s)\n", vulnerability.Name, coloredSeverity(severity))
if vulnerability.Description != "" {
fmt.Printf("%s\n\n", text.Indent(text.Wrap(vulnerability.Description, 80), "\t"))
}
fmt.Printf("\tPackage: %s @ %s\n", feature.Name, feature.Version)
if vulnerability.FixedBy != "" {
fmt.Printf("\tFixed version: %s\n", vulnerability.FixedBy)
}
if vulnerability.Link != "" {
fmt.Printf("\tLink: %s\n", vulnerability.Link)
}
fmt.Printf("\tLayer: %s\n", feature.AddedBy)
fmt.Println("")
}
if isSafe {
fmt.Printf("%s No vulnerabilities were detected in your image\n", color.GreenString("Success!"))
} else if !hasVisibleVulnerabilities {
fmt.Printf("%s No vulnerabilities matching the minimum severity level were detected in your image\n", color.YellowString("NOTE:"))
} else {
return fmt.Errorf("A total of %d vulnerabilities have been detected in your image", len(vulnerabilities))
}
return nil
}
func save(imageName, path string) error {
var stderr bytes.Buffer
save := exec.Command("docker", "save", imageName)
save.Stderr = &stderr
extract := exec.Command("tar", "xf", "-", "-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())
}
err = pipe.Close()
if err != nil {
return err
}
err = extract.Wait()
if err != nil {
return errors.New(stderr.String())
}
return nil
}
func historyFromManifest(path string) ([]string, error) {
mf, err := os.Open(path + "/manifest.json")
if err != nil {
return nil, err
}
defer mf.Close()
// https://github.com/docker/docker/blob/master/image/tarexport/tarexport.go#L17
type manifestItem struct {
Config string
RepoTags []string
Layers []string
}
var manifest []manifestItem
if err = json.NewDecoder(mf).Decode(&manifest); err != nil {
return nil, err
} else if len(manifest) != 1 {
return nil, err
}
var layers []string
for _, layer := range manifest[0].Layers {
layers = append(layers, strings.TrimSuffix(layer, "/layer.tar"))
}
return layers, nil
}
func historyFromCommand(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 listenHTTP(path, allowedHost string, ch chan error) {
restrictedFileServer := func(path, allowedHost string) http.Handler {
fc := func(w http.ResponseWriter, r *http.Request) {
host, _, err := net.SplitHostPort(r.RemoteAddr)
allowedIPs, err1 := net.LookupIP(allowedHost)
log.Printf("host: %s; ip: %s", host, allowedIPs[0])
fmt.Println(host,allowedIPs)
if err == nil && err1 == nil {
allowedIPs = append(allowedIPs, net.ParseIP("172.17.0.1"))
for _, a :=range allowedIPs {
if a.String() == host {
http.FileServer(http.Dir(path)).ServeHTTP(w, r)
return
}
}
}
w.WriteHeader(403)
}
return http.HandlerFunc(fc)
}
ch <- http.ListenAndServe(":"+strconv.Itoa(httpPort), restrictedFileServer(path, allowedHost))
}
func analyzeLayer(endpoint, path, layerName, parentLayerName string) error {
payload := v1.LayerEnvelope{
Layer: &v1.Layer{
Name: layerName,
Path: path,
ParentName: parentLayerName,
Format: "Docker",
},
}
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 getLayer(endpoint, layerID string) (v1.Layer, error) {
response, err := http.Get(endpoint + fmt.Sprintf(getLayerFeaturesURI, layerID))
if err != nil {
return v1.Layer{}, err
}
defer response.Body.Close()
if response.StatusCode != 200 {
body, _ := ioutil.ReadAll(response.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.Layer{}, err
} else if apiResponse.Error != nil {
return v1.Layer{}, errors.New(apiResponse.Error.Message)
}
return *apiResponse.Layer, nil
}
func coloredSeverity(severity types.Priority) string {
red := color.New(color.FgRed).SprintFunc()
yellow := color.New(color.FgYellow).SprintFunc()
white := color.New(color.FgWhite).SprintFunc()
switch severity {
case types.High, types.Critical:
return red(severity)
case types.Medium:
return yellow(severity)
default:
return white(severity)
}
}