131 lines
3.4 KiB
Go
131 lines
3.4 KiB
Go
package clair
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/docker/distribution/manifest/schema1"
|
|
"github.com/genuinetools/reg/registry"
|
|
)
|
|
|
|
// Vulnerabilities scans the given repo and tag
|
|
func (c *Clair) Vulnerabilities(r *registry.Registry, repo, tag string) (VulnerabilityReport, error) {
|
|
report := VulnerabilityReport{
|
|
RegistryURL: r.Domain,
|
|
Repo: repo,
|
|
Tag: tag,
|
|
Date: time.Now().Local().Format(time.RFC1123),
|
|
VulnsBySeverity: make(map[string][]Vulnerability),
|
|
}
|
|
|
|
// Get the v1 manifest to pass to clair.
|
|
m, err := r.ManifestV1(repo, tag)
|
|
if err != nil {
|
|
return report, fmt.Errorf("getting the v1 manifest for %s:%s failed: %v", repo, tag, err)
|
|
}
|
|
|
|
// Filter out the empty layers.
|
|
var filteredLayers []schema1.FSLayer
|
|
for _, layer := range m.FSLayers {
|
|
if layer.BlobSum != 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, tag)
|
|
return report, nil
|
|
}
|
|
|
|
for i := len(m.FSLayers) - 1; i >= 0; i-- {
|
|
// Form the clair layer.
|
|
l, err := c.NewClairLayer(r, repo, m.FSLayers, i)
|
|
if err != nil {
|
|
return report, err
|
|
}
|
|
|
|
// Post the layer.
|
|
if _, err := c.PostLayer(l); err != nil {
|
|
return report, err
|
|
}
|
|
}
|
|
|
|
vl, err := c.GetLayer(m.FSLayers[0].BlobSum.String(), false, true)
|
|
if err != nil {
|
|
return report, err
|
|
}
|
|
|
|
// Get the vulns.
|
|
for _, f := range vl.Features {
|
|
report.Vulns = append(report.Vulns, f.Vulnerabilities...)
|
|
}
|
|
|
|
vulnsBy := func(sev string, store map[string][]Vulnerability) []Vulnerability {
|
|
items, found := store[sev]
|
|
if !found {
|
|
items = make([]Vulnerability, 0)
|
|
store[sev] = items
|
|
}
|
|
return items
|
|
}
|
|
|
|
// group by severity
|
|
for _, v := range report.Vulns {
|
|
sevRow := vulnsBy(v.Severity, report.VulnsBySeverity)
|
|
report.VulnsBySeverity[v.Severity] = append(sevRow, v)
|
|
}
|
|
|
|
// calculate number of bad vulns
|
|
report.BadVulns = len(report.VulnsBySeverity["High"]) + len(report.VulnsBySeverity["Critical"]) + len(report.VulnsBySeverity["Defcon1"])
|
|
|
|
return report, nil
|
|
}
|
|
|
|
// NewClairLayer will form a layer struct required for a clar scan
|
|
func (c *Clair) NewClairLayer(r *registry.Registry, image string, fsLayers []schema1.FSLayer, index int) (*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()}, "/")
|
|
|
|
useBasicAuth := false
|
|
|
|
// get the token
|
|
token, err := r.Token(p)
|
|
if err != nil {
|
|
// if we get an error here of type: malformed auth challenge header: 'Basic realm="Registry Realm"'
|
|
// we need to use basic auth for the registry
|
|
if !strings.Contains(err.Error(), `malformed auth challenge header: 'Basic realm="Registry`) {
|
|
return nil, err
|
|
}
|
|
useBasicAuth = true
|
|
}
|
|
|
|
h := make(map[string]string)
|
|
if token != "" && !useBasicAuth {
|
|
h = map[string]string{
|
|
"Authorization": fmt.Sprintf("Bearer %s", token),
|
|
}
|
|
}
|
|
|
|
if token == "" || useBasicAuth {
|
|
c.Logf("clair.vulns using basic auth")
|
|
h = map[string]string{
|
|
"Authorization": fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(r.Username+":"+r.Password))),
|
|
}
|
|
}
|
|
|
|
return &Layer{
|
|
Name: fsLayers[index].BlobSum.String(),
|
|
Path: p,
|
|
ParentName: parentName,
|
|
Format: "Docker",
|
|
Headers: h,
|
|
}, nil
|
|
}
|