From 3075c58bc14f84f33522f2187e3b5104a9296f45 Mon Sep 17 00:00:00 2001 From: Jess Frazelle Date: Sun, 12 Feb 2017 18:45:47 -0800 Subject: [PATCH] add vulns command Signed-off-by: Jess Frazelle --- README.md | 1 + clair/clair.go | 2 + clair/layer.go | 30 +++++--- clair/types.go | 20 ++++++ main.go | 142 +++++++++++++++++++++++++++++++++++++ registry/manifest.go | 5 +- registry/ping.go | 3 - registry/registry.go | 14 ++-- registry/tokentransport.go | 34 +++++++++ 9 files changed, 231 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 4146e652..2a484faf 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ COMMANDS: delete, rm delete a specific reference of a repository list, ls list all repositories manifest get the json manifest for the specific reference of a repository + vulns get a vulnerability report for the image from CoreOS Clair tags get the tags for a repository download, layer download a layer for the specific reference of a repository help, h Shows a list of commands or help for one command diff --git a/clair/clair.go b/clair/clair.go index fd8a7578..317d3f66 100644 --- a/clair/clair.go +++ b/clair/clair.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "net/http" + "time" ) // Clair defines the client for retriving information from the clair API. @@ -42,6 +43,7 @@ func New(url string, debug bool) (*Clair, error) { registry := &Clair{ URL: url, Client: &http.Client{ + Timeout: time.Minute, Transport: errorTransport, }, Logf: logf, diff --git a/clair/layer.go b/clair/layer.go index 9fea1307..9457fcd8 100644 --- a/clair/layer.go +++ b/clair/layer.go @@ -8,38 +8,48 @@ import ( ) // GetLayer displays a Layer and optionally all of its features and vulnerabilities. -func (c *Clair) GetLayer(name string, features, vulnerabilities bool) (layer Layer, err error) { +func (c *Clair) GetLayer(name string, features, vulnerabilities bool) (*Layer, error) { url := c.url("/v1/layers/%s?features=%t&vulnerabilities=%t", name, features, vulnerabilities) c.Logf("clair.layers.get url=%s name=%s", url, name) - if _, err := c.getJSON(url, &layer); err != nil { - return layer, err + var respLayer layerEnvelope + if _, err := c.getJSON(url, &respLayer); err != nil { + return nil, err } - return layer, nil + if respLayer.Error != nil { + return nil, fmt.Errorf("clair error: %s", respLayer.Error.Message) + } + + return respLayer.Layer, nil } // PostLayer performs the analysis of a Layer from the provided path. -func (c *Clair) PostLayer(layer Layer) (respLayer Layer, err error) { +func (c *Clair) PostLayer(layer *Layer) (*Layer, error) { url := c.url("/v1/layers") c.Logf("clair.layers.post url=%s name=%s", url, layer.Name) - b, err := json.Marshal(layer) + b, err := json.Marshal(layerEnvelope{Layer: layer}) if err != nil { - return respLayer, err + return nil, err } resp, err := c.Client.Post(url, "application/json", bytes.NewReader(b)) if err != nil { - return respLayer, err + return nil, err } defer resp.Body.Close() + var respLayer layerEnvelope if err := json.NewDecoder(resp.Body).Decode(&respLayer); err != nil { - return respLayer, err + return nil, err } - return respLayer, err + if respLayer.Error != nil { + return nil, fmt.Errorf("clair error: %s", respLayer.Error.Message) + } + + return respLayer.Layer, err } // DeleteLayer removes a layer reference from clair. diff --git a/clair/types.go b/clair/types.go index a7fde75f..980ef604 100644 --- a/clair/types.go +++ b/clair/types.go @@ -1,5 +1,20 @@ package clair +const ( + // EmptyLayerBlobSum is the blob sum of empty layers. + EmptyLayerBlobSum = "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" +) + +var ( + // Priorities are the vulnerability priority labels. + Priorities = []string{"Unknown", "Negligible", "Low", "Medium", "High", "Critical", "Defcon1"} +) + +// Error describes the structure of a clair error. +type Error struct { + Message string `json:"Message,omitempty"` +} + // Layer represents an image layer. type Layer struct { Name string `json:"Name,omitempty"` @@ -12,6 +27,11 @@ type Layer struct { Features []feature `json:"Features,omitempty"` } +type layerEnvelope struct { + Layer *Layer `json:"Layer,omitempty"` + Error *Error `json:"Error,omitempty"` +} + // Vulnerability represents vulnerability entity returned by Clair. type Vulnerability struct { Name string `json:"Name,omitempty"` diff --git a/main.go b/main.go index 7562bf84..db18338f 100644 --- a/main.go +++ b/main.go @@ -11,8 +11,10 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/distribution/digest" + "github.com/docker/distribution/manifest/schema1" "github.com/docker/docker/cliconfig" "github.com/docker/engine-api/types" + "github.com/jessfraz/reg/clair" "github.com/jessfraz/reg/registry" "github.com/urfave/cli" ) @@ -156,6 +158,120 @@ func main() { return nil }, }, + { + Name: "vulns", + Usage: "get a vulnerability report for the image from CoreOS Clair", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "clair", + Usage: "url to clair instance", + }, + }, + Action: func(c *cli.Context) error { + if c.String("clair") == "" { + return errors.New("clair url cannot be empty, pass --clair") + } + + repo, ref, err := getRepoAndRef(c) + if err != nil { + return err + } + + // get the manifest + m, err := r.ManifestV1(repo, ref) + if err != nil { + return err + } + + // filter out the empty layers + var filteredLayers []schema1.FSLayer + for _, layer := range m.FSLayers { + if layer.BlobSum != clair.EmptyLayerBlobSum { + filteredLayers = append(filteredLayers, layer) + } + } + m.FSLayers = filteredLayers + if len(m.FSLayers) == 0 { + fmt.Printf("No need to analyse image %s:%s as there is no non-emtpy layer", repo, ref) + return nil + } + + // initialize clair + cr, err := clair.New(c.String("clair"), c.GlobalBool("debug")) + if err != nil { + return err + } + + for i := len(m.FSLayers) - 1; i >= 0; i-- { + // form the clair layer + l, err := newClairLayer(r, repo, m.FSLayers, i) + if err != nil { + return err + } + + // post the layer + if _, err := cr.PostLayer(l); err != nil { + return err + } + } + + vl, err := cr.GetLayer(m.FSLayers[0].BlobSum.String(), false, true) + if err != nil { + return err + } + + // get the vulns + var vulns []clair.Vulnerability + for _, f := range vl.Features { + for _, v := range f.Vulnerabilities { + vulns = append(vulns, v) + } + } + fmt.Printf("Found %d vulnerabilities \n", len(vulns)) + + vulnsBy := func(sev string, store map[string][]clair.Vulnerability) []clair.Vulnerability { + items, found := store[sev] + if !found { + items = make([]clair.Vulnerability, 0) + store[sev] = items + } + return items + } + + // group by severity + store := make(map[string][]clair.Vulnerability) + for _, v := range vulns { + sevRow := vulnsBy(v.Severity, store) + store[v.Severity] = append(sevRow, v) + } + + // iterate over the priorities list + iteratePriorities := func(f func(sev string)) { + for _, sev := range clair.Priorities { + if len(store[sev]) != 0 { + f(sev) + } + } + } + iteratePriorities(func(sev string) { + for _, v := range store[sev] { + fmt.Printf("%s: [%s] \n%s\n%s\n", v.Name, v.Severity, v.Description, v.Link) + fmt.Println("-----------------------------------------") + } + }) + iteratePriorities(func(sev string) { + fmt.Printf("%s: %d\n", sev, len(store[sev])) + }) + + // return an error if there are more than 10 bad vulns + lenBadVulns := len(store["High"]) + len(store["Critical"]) + len(store["Defcon1"]) + if lenBadVulns > 10 { + logrus.Fatalf("%d bad vunerabilities found", lenBadVulns) + } + + return nil + }, + }, { Name: "tags", Usage: "get the tags for a repository", @@ -279,3 +395,29 @@ func getRepoAndRef(c *cli.Context) (repo, ref string, err error) { return } + +func newClairLayer(r *registry.Registry, image string, fsLayers []schema1.FSLayer, index int) (*clair.Layer, error) { + var parentName string + if index < len(fsLayers)-1 { + parentName = fsLayers[index+1].BlobSum.String() + } + + // form the path + p := strings.Join([]string{r.URL, "v2", image, "blobs", fsLayers[index].BlobSum.String()}, "/") + + // get the token + token, err := r.Token(p) + if err != nil { + return nil, err + } + + return &clair.Layer{ + Name: fsLayers[index].BlobSum.String(), + Path: p, + ParentName: parentName, + Format: "Docker", + Headers: map[string]string{ + "Authorization": fmt.Sprintf("Bearer %s", token), + }, + }, nil +} diff --git a/registry/manifest.go b/registry/manifest.go index 83f16e7c..693493ba 100644 --- a/registry/manifest.go +++ b/registry/manifest.go @@ -24,13 +24,14 @@ func (r *Registry) Manifest(repository, ref string) (interface{}, error) { } if m.Versioned.SchemaVersion == 1 { - return r.v1Manifest(repository, ref) + return r.ManifestV1(repository, ref) } return m, nil } -func (r *Registry) v1Manifest(repository, ref string) (schema1.SignedManifest, error) { +// ManifestV1 gets the registry v1 manifest. +func (r *Registry) ManifestV1(repository, ref string) (schema1.SignedManifest, error) { url := r.url("/v2/%s/manifests/%s", repository, ref) r.Logf("registry.manifests url=%s repository=%s ref=%s", url, repository, ref) diff --git a/registry/ping.go b/registry/ping.go index 57dc3441..2e984ace 100644 --- a/registry/ping.go +++ b/registry/ping.go @@ -7,9 +7,6 @@ func (r *Registry) Ping() error { resp, err := r.Client.Get(url) if resp != nil { defer resp.Body.Close() - } - if err != nil { - } return err } diff --git a/registry/registry.go b/registry/registry.go index 659a3cd4..ad43e93a 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -13,10 +13,12 @@ import ( // Registry defines the client for retriving information from the registry API. type Registry struct { - URL string - Domain string - Client *http.Client - Logf LogfCallback + URL string + Domain string + Username string + Password string + Client *http.Client + Logf LogfCallback } // LogfCallback is the callback for formatting logs. @@ -78,7 +80,9 @@ func newFromTransport(auth types.AuthConfig, transport http.RoundTripper, debug Client: &http.Client{ Transport: errorTransport, }, - Logf: logf, + Username: auth.Username, + Password: auth.Password, + Logf: logf, } if err := registry.Ping(); err != nil { diff --git a/registry/tokentransport.go b/registry/tokentransport.go index f58f4baa..44f6391b 100644 --- a/registry/tokentransport.go +++ b/registry/tokentransport.go @@ -112,3 +112,37 @@ func isTokenDemand(resp *http.Response) (*authService, error) { } return parseAuthHeader(resp.Header) } + +// Token returns the required token for the specific resource url. +func (r *Registry) Token(url string) (string, error) { + r.Logf("registry.token url=%s", url) + + resp, err := http.Get(url) + if err != nil { + return "", err + } + defer resp.Body.Close() + + a, err := parseAuthHeader(resp.Header) + if err != nil { + return "", err + } + + authReq, err := a.Request(r.Username, r.Password) + resp, err = http.DefaultClient.Do(authReq) + if err != nil { + return "", err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", err + } + + var authToken authToken + if err := json.NewDecoder(resp.Body).Decode(&authToken); err != nil { + return "", err + } + + return authToken.Token, nil +}