add vulns command

Signed-off-by: Jess Frazelle <acidburn@google.com>
This commit is contained in:
Jess Frazelle 2017-02-12 18:45:47 -08:00
parent f0968a951b
commit 3075c58bc1
No known key found for this signature in database
GPG key ID: 18F3685C0022BFF3
9 changed files with 231 additions and 20 deletions

View file

@ -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

View file

@ -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,

View file

@ -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.

View file

@ -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"`

142
main.go
View file

@ -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
}

View file

@ -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)

View file

@ -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
}

View file

@ -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 {

View file

@ -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
}