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
|
||||
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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
142
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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@ import (
|
|||
type Registry struct {
|
||||
URL string
|
||||
Domain string
|
||||
Username string
|
||||
Password string
|
||||
Client *http.Client
|
||||
Logf LogfCallback
|
||||
}
|
||||
|
@ -78,6 +80,8 @@ func newFromTransport(auth types.AuthConfig, transport http.RoundTripper, debug
|
|||
Client: &http.Client{
|
||||
Transport: errorTransport,
|
||||
},
|
||||
Username: auth.Username,
|
||||
Password: auth.Password,
|
||||
Logf: logf,
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue