2015-11-29 20:56:34 +00:00
// 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.
2015-11-20 00:27:49 +00:00
package main
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"flag"
"fmt"
"io/ioutil"
2016-03-17 19:40:42 +00:00
"log"
2016-03-08 06:41:14 +00:00
"net"
2015-11-20 00:27:49 +00:00
"net/http"
"os"
"os/exec"
2016-03-17 19:40:42 +00:00
"sort"
2015-11-20 00:27:49 +00:00
"strconv"
"strings"
"time"
2016-02-16 07:22:25 +00:00
"github.com/coreos/clair/api/v1"
2016-03-17 15:41:00 +00:00
"github.com/coreos/clair/utils/types"
2016-03-17 17:07:28 +00:00
"github.com/fatih/color"
2016-03-17 19:40:42 +00:00
"github.com/kr/text"
2015-11-20 00:27:49 +00:00
)
const (
2016-02-16 07:22:25 +00:00
postLayerURI = "/v1/layers"
2016-02-24 21:43:19 +00:00
getLayerFeaturesURI = "/v1/layers/%s?vulnerabilities"
2016-02-16 07:22:25 +00:00
httpPort = 9279
2015-11-20 00:27:49 +00:00
)
2016-03-17 17:07:28 +00:00
var (
2016-03-23 19:02:26 +00:00
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)" )
2016-03-17 17:07:28 +00:00
)
2016-03-17 19:40:42 +00:00
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 ] )
}
2015-11-20 00:27:49 +00:00
func main ( ) {
2016-02-24 21:43:19 +00:00
// Parse command-line arguments.
2015-11-20 00:27:49 +00:00
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 ]
2016-03-23 19:02:26 +00:00
minSeverity := types . Priority ( * flagMinimumSeverity )
2016-03-17 15:41:00 +00:00
if ! minSeverity . IsValid ( ) {
flag . Usage ( )
os . Exit ( 1 )
}
2016-03-23 19:02:26 +00:00
if * flagColorMode == "never" {
2016-03-17 17:07:28 +00:00
color . NoColor = true
2016-03-23 19:02:26 +00:00
} else if * flagColorMode == "always" {
2016-03-17 23:19:29 +00:00
color . NoColor = false
2016-03-17 17:07:28 +00:00
}
2016-03-23 19:02:26 +00:00
err := AnalyzeLocalImage ( imageName , minSeverity , * flagEndpoint , * flagMyAddress )
if err != nil {
log . Fatal ( err )
}
}
func AnalyzeLocalImage ( imageName string , minSeverity types . Priority , endpoint , myAddress string ) error {
2016-02-24 21:43:19 +00:00
// Save image.
2016-03-17 19:40:42 +00:00
log . Printf ( "Saving %s to local disk (this may take some time)" , imageName )
2015-11-20 00:27:49 +00:00
path , err := save ( imageName )
defer os . RemoveAll ( path )
if err != nil {
2016-03-23 19:02:26 +00:00
return fmt . Errorf ( "Could not save image: %s" , err )
2015-11-20 00:27:49 +00:00
}
2016-02-24 21:43:19 +00:00
// Retrieve history.
2016-03-17 19:40:42 +00:00
log . Println ( "Retrieving image history" )
2016-02-14 05:54:21 +00:00
layerIDs , err := historyFromManifest ( path )
if err != nil {
layerIDs , err = historyFromCommand ( imageName )
}
2015-11-20 00:27:49 +00:00
if err != nil || len ( layerIDs ) == 0 {
2016-03-23 19:02:26 +00:00
return fmt . Errorf ( "Could not get image's history: %s" , err )
2015-11-20 00:27:49 +00:00
}
2016-02-24 21:43:19 +00:00
// Setup a simple HTTP server if Clair is not local.
2016-03-23 19:02:26 +00:00
if ! strings . Contains ( endpoint , "127.0.0.1" ) && ! strings . Contains ( endpoint , "localhost" ) {
allowedHost := strings . TrimPrefix ( endpoint , "http://" )
2016-02-24 21:43:19 +00:00
portIndex := strings . Index ( allowedHost , ":" )
if portIndex >= 0 {
allowedHost = allowedHost [ : portIndex ]
}
2015-11-20 00:27:49 +00:00
2016-03-23 19:02:26 +00:00
log . Printf ( "Setting up HTTP server (allowing: %s)\n" , allowedHost )
ch := make ( chan error )
go listenHTTP ( path , 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
}
2015-11-20 00:27:49 +00:00
2016-03-23 19:02:26 +00:00
path = "http://" + myAddress + ":" + strconv . Itoa ( httpPort )
2015-11-20 00:27:49 +00:00
}
2016-02-24 21:43:19 +00:00
// Analyze layers.
2016-03-17 19:40:42 +00:00
log . Printf ( "Analyzing %d layers... \n" , len ( layerIDs ) )
2015-11-20 00:27:49 +00:00
for i := 0 ; i < len ( layerIDs ) ; i ++ {
2016-03-17 19:40:42 +00:00
log . Printf ( "Analyzing %s\n" , layerIDs [ i ] )
2015-11-20 00:27:49 +00:00
var err error
if i > 0 {
2016-03-23 19:02:26 +00:00
err = analyzeLayer ( endpoint , path + "/" + layerIDs [ i ] + "/layer.tar" , layerIDs [ i ] , layerIDs [ i - 1 ] )
2015-11-20 00:27:49 +00:00
} else {
2016-03-23 19:02:26 +00:00
err = analyzeLayer ( endpoint , path + "/" + layerIDs [ i ] + "/layer.tar" , layerIDs [ i ] , "" )
2015-11-20 00:27:49 +00:00
}
if err != nil {
2016-03-23 19:02:26 +00:00
return fmt . Errorf ( "Could not analyze layer: %s" , err )
2015-11-20 00:27:49 +00:00
}
}
2016-02-24 21:43:19 +00:00
// Get vulnerabilities.
2016-03-17 19:40:42 +00:00
log . Println ( "Retrieving image's vulnerabilities" )
2016-03-23 19:02:26 +00:00
layer , err := getLayer ( endpoint , layerIDs [ len ( layerIDs ) - 1 ] )
2015-11-20 00:27:49 +00:00
if err != nil {
2016-03-23 19:02:26 +00:00
return fmt . Errorf ( "Could not get layer information: %s" , err )
2016-02-24 21:43:19 +00:00
}
// Print report.
2016-03-17 19:40:42 +00:00
fmt . Printf ( "Clair report for image %s (%s)\n" , imageName , time . Now ( ) . UTC ( ) )
2016-02-24 21:43:19 +00:00
if len ( layer . Features ) == 0 {
2016-03-17 19:40:42 +00:00
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:" ) )
2016-03-23 19:02:26 +00:00
return nil
2016-02-16 07:22:25 +00:00
}
isSafe := true
2016-03-17 19:40:42 +00:00
hasVisibleVulnerabilities := false
var vulnerabilities = make ( [ ] vulnerabilityInfo , 0 )
2016-02-24 21:43:19 +00:00
for _ , feature := range layer . Features {
if len ( feature . Vulnerabilities ) > 0 {
2016-02-16 07:22:25 +00:00
for _ , vulnerability := range feature . Vulnerabilities {
2016-03-17 17:07:28 +00:00
severity := types . Priority ( vulnerability . Severity )
2016-03-17 19:40:42 +00:00
isSafe = false
2016-03-17 17:07:28 +00:00
if minSeverity . Compare ( severity ) > 0 {
2016-03-17 15:41:00 +00:00
continue
}
2016-03-17 19:40:42 +00:00
hasVisibleVulnerabilities = true
vulnerabilities = append ( vulnerabilities , vulnerabilityInfo { vulnerability , feature , severity } )
}
}
}
2016-03-17 17:03:12 +00:00
2016-03-17 19:40:42 +00:00
// Sort vulnerabilitiy by severity.
priority := func ( v1 , v2 vulnerabilityInfo ) bool {
return v1 . severity . Compare ( v2 . severity ) >= 0
}
2016-03-17 15:41:00 +00:00
2016-03-17 19:40:42 +00:00
By ( priority ) . Sort ( vulnerabilities )
2016-02-24 21:43:19 +00:00
2016-03-17 19:40:42 +00:00
for _ , vulnerabilityInfo := range vulnerabilities {
vulnerability := vulnerabilityInfo . vulnerability
feature := vulnerabilityInfo . feature
severity := vulnerabilityInfo . severity
2016-02-24 21:43:19 +00:00
2016-03-17 19:40:42 +00:00
fmt . Printf ( "%s (%s)\n" , vulnerability . Name , coloredSeverity ( severity ) )
2016-02-24 21:43:19 +00:00
2016-03-17 19:40:42 +00:00
if vulnerability . Description != "" {
fmt . Printf ( "%s\n\n" , text . Indent ( text . Wrap ( vulnerability . Description , 80 ) , "\t" ) )
}
2016-02-24 21:43:19 +00:00
2016-03-17 19:40:42 +00:00
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 )
2016-02-16 07:22:25 +00:00
}
2016-03-17 19:40:42 +00:00
fmt . Printf ( "\tLayer: %s\n" , feature . AddedBy )
fmt . Println ( "" )
2015-11-20 00:27:49 +00:00
}
2016-02-24 21:43:19 +00:00
2016-02-16 07:22:25 +00:00
if isSafe {
2016-03-17 19:40:42 +00:00
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:" ) )
2015-11-20 00:27:49 +00:00
}
2016-03-23 19:02:26 +00:00
return nil
2015-11-20 00:27:49 +00:00
}
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
2015-11-23 14:01:58 +00:00
extract := exec . Command ( "tar" , "xf" , "-" , "-C" + path )
2015-11-20 00:27:49 +00:00
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 ( ) )
}
2015-11-24 16:02:13 +00:00
err = pipe . Close ( )
if err != nil {
return "" , err
}
err = extract . Wait ( )
if err != nil {
return "" , errors . New ( stderr . String ( ) )
}
2015-11-20 00:27:49 +00:00
return path , nil
}
2016-02-14 05:54:21 +00:00
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 ) {
2015-11-20 00:27:49 +00:00
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
}
2016-03-23 19:02:26 +00:00
func listenHTTP ( path , allowedHost string , ch chan error ) {
2016-02-24 21:43:19 +00:00
restrictedFileServer := func ( path , allowedHost string ) http . Handler {
fc := func ( w http . ResponseWriter , r * http . Request ) {
2016-03-08 06:41:14 +00:00
host , _ , err := net . SplitHostPort ( r . RemoteAddr )
if err == nil && strings . EqualFold ( host , allowedHost ) {
2016-02-24 21:43:19 +00:00
http . FileServer ( http . Dir ( path ) ) . ServeHTTP ( w , r )
return
}
w . WriteHeader ( 403 )
}
return http . HandlerFunc ( fc )
}
2016-03-23 19:02:26 +00:00
ch <- http . ListenAndServe ( ":" + strconv . Itoa ( httpPort ) , restrictedFileServer ( path , allowedHost ) )
2016-02-24 21:43:19 +00:00
}
2016-02-16 07:22:25 +00:00
func analyzeLayer ( endpoint , path , layerName , parentLayerName string ) error {
2016-02-24 21:43:19 +00:00
payload := v1 . LayerEnvelope {
Layer : & v1 . Layer {
Name : layerName ,
Path : path ,
ParentName : parentLayerName ,
Format : "Docker" ,
} ,
}
2015-11-20 00:27:49 +00:00
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 )
2016-03-17 19:40:42 +00:00
return fmt . Errorf ( "Got response %d with message %s" , response . StatusCode , string ( body ) )
2015-11-20 00:27:49 +00:00
}
return nil
}
2016-02-24 21:43:19 +00:00
func getLayer ( endpoint , layerID string ) ( v1 . Layer , error ) {
2016-02-16 07:22:25 +00:00
response , err := http . Get ( endpoint + fmt . Sprintf ( getLayerFeaturesURI , layerID ) )
2015-11-20 00:27:49 +00:00
if err != nil {
2016-02-24 21:43:19 +00:00
return v1 . Layer { } , err
2015-11-20 00:27:49 +00:00
}
defer response . Body . Close ( )
if response . StatusCode != 200 {
body , _ := ioutil . ReadAll ( response . Body )
2016-03-17 19:40:42 +00:00
err := fmt . Errorf ( "Got response %d with message %s" , response . StatusCode , string ( body ) )
2016-02-24 21:43:19 +00:00
return v1 . Layer { } , err
2015-11-20 00:27:49 +00:00
}
2016-02-16 07:22:25 +00:00
var apiResponse v1 . LayerEnvelope
if err = json . NewDecoder ( response . Body ) . Decode ( & apiResponse ) ; err != nil {
2016-02-24 21:43:19 +00:00
return v1 . Layer { } , err
2016-02-16 07:22:25 +00:00
} else if apiResponse . Error != nil {
2016-02-24 21:43:19 +00:00
return v1 . Layer { } , errors . New ( apiResponse . Error . Message )
2015-11-20 00:27:49 +00:00
}
2016-02-24 21:43:19 +00:00
return * apiResponse . Layer , nil
2015-11-20 00:27:49 +00:00
}
2016-03-17 17:07:28 +00:00
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 )
}
}