mirror of
https://github.com/genuinetools/reg.git
synced 2024-09-28 03:36:19 -04:00
add vulns command
Signed-off-by: Jess Frazelle <acidburn@google.com>
This commit is contained in:
parent
f0968a951b
commit
3075c58bc1
9 changed files with 231 additions and 20 deletions
|
@ -55,6 +55,7 @@ COMMANDS:
|
||||||
delete, rm delete a specific reference of a repository
|
delete, rm delete a specific reference of a repository
|
||||||
list, ls list all repositories
|
list, ls list all repositories
|
||||||
manifest get the json manifest for the specific reference of a repository
|
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
|
tags get the tags for a repository
|
||||||
download, layer download a layer for the specific reference of 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
|
help, h Shows a list of commands or help for one command
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Clair defines the client for retriving information from the clair API.
|
// 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{
|
registry := &Clair{
|
||||||
URL: url,
|
URL: url,
|
||||||
Client: &http.Client{
|
Client: &http.Client{
|
||||||
|
Timeout: time.Minute,
|
||||||
Transport: errorTransport,
|
Transport: errorTransport,
|
||||||
},
|
},
|
||||||
Logf: logf,
|
Logf: logf,
|
||||||
|
|
|
@ -8,38 +8,48 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetLayer displays a Layer and optionally all of its features and vulnerabilities.
|
// 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)
|
url := c.url("/v1/layers/%s?features=%t&vulnerabilities=%t", name, features, vulnerabilities)
|
||||||
c.Logf("clair.layers.get url=%s name=%s", url, name)
|
c.Logf("clair.layers.get url=%s name=%s", url, name)
|
||||||
|
|
||||||
if _, err := c.getJSON(url, &layer); err != nil {
|
var respLayer layerEnvelope
|
||||||
return layer, err
|
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.
|
// 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")
|
url := c.url("/v1/layers")
|
||||||
c.Logf("clair.layers.post url=%s name=%s", url, layer.Name)
|
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 {
|
if err != nil {
|
||||||
return respLayer, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := c.Client.Post(url, "application/json", bytes.NewReader(b))
|
resp, err := c.Client.Post(url, "application/json", bytes.NewReader(b))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return respLayer, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var respLayer layerEnvelope
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&respLayer); err != nil {
|
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.
|
// DeleteLayer removes a layer reference from clair.
|
||||||
|
|
|
@ -1,5 +1,20 @@
|
||||||
package clair
|
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.
|
// Layer represents an image layer.
|
||||||
type Layer struct {
|
type Layer struct {
|
||||||
Name string `json:"Name,omitempty"`
|
Name string `json:"Name,omitempty"`
|
||||||
|
@ -12,6 +27,11 @@ type Layer struct {
|
||||||
Features []feature `json:"Features,omitempty"`
|
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.
|
// Vulnerability represents vulnerability entity returned by Clair.
|
||||||
type Vulnerability struct {
|
type Vulnerability struct {
|
||||||
Name string `json:"Name,omitempty"`
|
Name string `json:"Name,omitempty"`
|
||||||
|
|
142
main.go
142
main.go
|
@ -11,8 +11,10 @@ import (
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
"github.com/docker/distribution/digest"
|
"github.com/docker/distribution/digest"
|
||||||
|
"github.com/docker/distribution/manifest/schema1"
|
||||||
"github.com/docker/docker/cliconfig"
|
"github.com/docker/docker/cliconfig"
|
||||||
"github.com/docker/engine-api/types"
|
"github.com/docker/engine-api/types"
|
||||||
|
"github.com/jessfraz/reg/clair"
|
||||||
"github.com/jessfraz/reg/registry"
|
"github.com/jessfraz/reg/registry"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
@ -156,6 +158,120 @@ func main() {
|
||||||
return nil
|
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",
|
Name: "tags",
|
||||||
Usage: "get the tags for a repository",
|
Usage: "get the tags for a repository",
|
||||||
|
@ -279,3 +395,29 @@ func getRepoAndRef(c *cli.Context) (repo, ref string, err error) {
|
||||||
|
|
||||||
return
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -24,13 +24,14 @@ func (r *Registry) Manifest(repository, ref string) (interface{}, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.Versioned.SchemaVersion == 1 {
|
if m.Versioned.SchemaVersion == 1 {
|
||||||
return r.v1Manifest(repository, ref)
|
return r.ManifestV1(repository, ref)
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
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)
|
url := r.url("/v2/%s/manifests/%s", repository, ref)
|
||||||
r.Logf("registry.manifests url=%s repository=%s ref=%s", url, repository, ref)
|
r.Logf("registry.manifests url=%s repository=%s ref=%s", url, repository, ref)
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,6 @@ func (r *Registry) Ping() error {
|
||||||
resp, err := r.Client.Get(url)
|
resp, err := r.Client.Get(url)
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,10 +13,12 @@ import (
|
||||||
|
|
||||||
// Registry defines the client for retriving information from the registry API.
|
// Registry defines the client for retriving information from the registry API.
|
||||||
type Registry struct {
|
type Registry struct {
|
||||||
URL string
|
URL string
|
||||||
Domain string
|
Domain string
|
||||||
Client *http.Client
|
Username string
|
||||||
Logf LogfCallback
|
Password string
|
||||||
|
Client *http.Client
|
||||||
|
Logf LogfCallback
|
||||||
}
|
}
|
||||||
|
|
||||||
// LogfCallback is the callback for formatting logs.
|
// LogfCallback is the callback for formatting logs.
|
||||||
|
@ -78,7 +80,9 @@ func newFromTransport(auth types.AuthConfig, transport http.RoundTripper, debug
|
||||||
Client: &http.Client{
|
Client: &http.Client{
|
||||||
Transport: errorTransport,
|
Transport: errorTransport,
|
||||||
},
|
},
|
||||||
Logf: logf,
|
Username: auth.Username,
|
||||||
|
Password: auth.Password,
|
||||||
|
Logf: logf,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := registry.Ping(); err != nil {
|
if err := registry.Ping(); err != nil {
|
||||||
|
|
|
@ -112,3 +112,37 @@ func isTokenDemand(resp *http.Response) (*authService, error) {
|
||||||
}
|
}
|
||||||
return parseAuthHeader(resp.Header)
|
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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue