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-08 06:41:14 +00:00
"net"
2015-11-20 00:27:49 +00:00
"net/http"
"os"
"os/exec"
"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"
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 (
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" )
minimumSeverity = flag . String ( "minimum-severity" , "Negligible" , "Minimum severity of vulnerabilities to show (Unknown, Negligible, Low, Medium, High, Critical, Defcon1)" )
flagNoColor = flag . Bool ( "no-color" , false , "Disable color output" )
)
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-17 15:41:00 +00:00
minSeverity := types . Priority ( * minimumSeverity )
if ! minSeverity . IsValid ( ) {
flag . Usage ( )
os . Exit ( 1 )
}
2016-03-17 17:07:28 +00:00
if * flagNoColor {
color . NoColor = true
}
2016-02-24 21:43:19 +00:00
// Save image.
2015-11-20 00:27:49 +00:00
fmt . Printf ( "Saving %s\n" , imageName )
path , err := save ( imageName )
defer os . RemoveAll ( path )
if err != nil {
2016-02-24 21:43:19 +00:00
fmt . Printf ( "- Could not save image: %s\n" , err )
os . Exit ( 1 )
2015-11-20 00:27:49 +00:00
}
2016-02-24 21:43:19 +00:00
// Retrieve history.
2015-11-20 00:27:49 +00:00
fmt . Println ( "Getting image's 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-02-24 21:43:19 +00:00
fmt . Printf ( "- Could not get image's history: %s\n" , err )
os . Exit ( 1 )
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.
2015-11-20 00:27:49 +00:00
if ! strings . Contains ( * endpoint , "127.0.0.1" ) && ! strings . Contains ( * endpoint , "localhost" ) {
2016-02-24 21:43:19 +00:00
allowedHost := strings . TrimPrefix ( * endpoint , "http://" )
portIndex := strings . Index ( allowedHost , ":" )
if portIndex >= 0 {
allowedHost = allowedHost [ : portIndex ]
}
2015-11-20 00:27:49 +00:00
2016-02-24 21:43:19 +00:00
go listenHTTP ( path , allowedHost )
2015-11-20 00:27:49 +00:00
path = "http://" + * myAddress + ":" + strconv . Itoa ( httpPort )
time . Sleep ( 200 * time . Millisecond )
}
2016-02-24 21:43:19 +00:00
// Analyze layers.
2015-11-20 00:27:49 +00:00
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 {
2016-02-24 21:43:19 +00:00
fmt . Printf ( "- Could not analyze layer: %s\n" , err )
os . Exit ( 1 )
2015-11-20 00:27:49 +00:00
}
}
2016-02-24 21:43:19 +00:00
// Get vulnerabilities.
fmt . Println ( "Getting image's vulnerabilities" )
layer , err := getLayer ( * endpoint , layerIDs [ len ( layerIDs ) - 1 ] )
2015-11-20 00:27:49 +00:00
if err != nil {
2016-02-24 21:43:19 +00:00
fmt . Printf ( "- Could not get layer information: %s\n" , err )
os . Exit ( 1 )
}
// Print report.
fmt . Printf ( "\n# Clair report for image %s (%s)\n" , imageName , time . Now ( ) . UTC ( ) )
if len ( layer . Features ) == 0 {
fmt . Println ( "No feature has been detected on the image." )
fmt . Println ( "This usually means that the image isn't supported by Clair." )
os . Exit ( 0 )
2016-02-16 07:22:25 +00:00
}
isSafe := true
2016-02-24 21:43:19 +00:00
for _ , feature := range layer . Features {
if len ( feature . Vulnerabilities ) > 0 {
2016-03-17 15:41:00 +00:00
isFirstVulnerability := true
2016-02-24 21:43:19 +00:00
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 )
if minSeverity . Compare ( severity ) > 0 {
2016-03-17 15:41:00 +00:00
continue
}
if isFirstVulnerability {
isSafe = false
isFirstVulnerability = false
2016-03-17 17:03:12 +00:00
fmt . Printf ( "## Feature: %s %s (%s)\n" , feature . Name , feature . Version , feature . NamespaceName )
2016-03-17 17:07:28 +00:00
fmt . Printf ( "- Added by layer: %s\n" , feature . AddedBy )
2016-03-17 15:41:00 +00:00
}
2016-03-17 17:07:28 +00:00
fmt . Printf ( "### (%s) %s\n" , coloredSeverity ( severity ) , vulnerability . Name )
2016-02-24 21:43:19 +00:00
if vulnerability . Description != "" {
2016-03-17 17:07:28 +00:00
fmt . Printf ( "- Link: %s\n" , vulnerability . Link )
2016-02-24 21:43:19 +00:00
}
if vulnerability . Link != "" {
2016-03-17 17:07:28 +00:00
fmt . Printf ( "- Description: %s\n" , vulnerability . Description )
2016-02-24 21:43:19 +00:00
}
if vulnerability . FixedBy != "" {
2016-03-17 17:07:28 +00:00
fmt . Printf ( "- Fixed version: %s\n" , vulnerability . FixedBy )
2016-02-24 21:43:19 +00:00
}
if len ( vulnerability . Metadata ) > 0 {
2016-03-17 17:07:28 +00:00
fmt . Printf ( "- Metadata: %+v\n" , vulnerability . Metadata )
2016-02-24 21:43:19 +00:00
}
2016-02-16 07:22:25 +00:00
}
}
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-02-24 21:43:19 +00:00
fmt . Println ( "\nBravo, your image looks SAFE !" )
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-02-24 21:43:19 +00:00
func listenHTTP ( path , allowedHost string ) {
fmt . Printf ( "Setting up HTTP server (allowing: %s)\n" , allowedHost )
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 )
}
err := http . ListenAndServe ( ":" + strconv . Itoa ( httpPort ) , restrictedFileServer ( path , allowedHost ) )
if err != nil {
fmt . Printf ( "- An error occurs with the HTTP server: %s\n" , err )
os . Exit ( 1 )
}
}
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-02-24 21:43:19 +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-02-24 21:43:19 +00:00
err := fmt . Errorf ( "- Got response %d with message %s" , response . StatusCode , string ( body ) )
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 )
}
}