Skip to content

Commit

Permalink
Initial version of Clair API V3 support
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexey Miroshkin committed Dec 1, 2017
1 parent 5c1e58f commit 9466589
Show file tree
Hide file tree
Showing 4 changed files with 248 additions and 110 deletions.
198 changes: 198 additions & 0 deletions clair/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package clair

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"time"

"github.com/coreos/clair/api/v3/clairpb"
"github.com/optiopay/klar/docker"
"github.com/optiopay/klar/utils"
"google.golang.org/grpc"
)

type apiV1 struct {
url string
client http.Client
}

type apiV3 struct {
client clairpb.AncestryServiceClient
}

func newAPI(url string, version int) (API, error) {
if version < 3 {
return newAPIV1(url), nil
}
return newAPIV3(url)
}

func newAPIV1(url string) *apiV1 {
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
url = fmt.Sprintf("http://%s", url)
}
if strings.LastIndex(url, ":") < 6 {
url = fmt.Sprintf("%s:6060", url)
}
return &apiV1{
url: url,
client: http.Client{
Timeout: time.Minute,
},
}
}

func newAPIV3(url string) (*apiV3, error) {
if i := strings.Index(url, "://"); i != -1 {
runes := []rune(url)
url = string(runes[i+3:])
}
fmt.Printf("URL: %q\n", url)
conn, err := grpc.Dial(url, grpc.WithInsecure())
if err != nil {
return nil, fmt.Errorf("did not connect to %s: %v", url, err)
}
fmt.Println("Created GRPC client")
return &apiV3{clairpb.NewAncestryServiceClient(conn)}, nil
}

func (a *apiV1) Push(image *docker.Image) error {
for i := 0; i < len(image.FsLayers); i++ {
layer := newLayer(image, i)
if err := a.pushLayer(layer); err != nil {
return err
}
}
return nil
}

func (a *apiV1) pushLayer(layer *layer) error {
envelope := layerEnvelope{Layer: layer}
reqBody, err := json.Marshal(envelope)
if err != nil {
return fmt.Errorf("can't serialze push request: %s", err)
}
url := fmt.Sprintf("%s/v1/layers", a.url)
request, err := http.NewRequest("POST", url, bytes.NewBuffer(reqBody))
if err != nil {
return fmt.Errorf("can't create a push request: %s", err)
}
request.Header.Set("Content-Type", "application/json")
utils.DumpRequest(request)
response, err := a.client.Do(request)
if err != nil {
return fmt.Errorf("can't push layer to Clair: %s", err)
}
utils.DumpResponse(response)
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return fmt.Errorf("can't read clair response : %s", err)
}
if response.StatusCode != http.StatusCreated {
var lerr layerError
err = json.Unmarshal(body, &lerr)
if err != nil {
return fmt.Errorf("can't even read an error message: %s", err)
}
return fmt.Errorf("push error %d: %s", response.StatusCode, string(body))
}
return nil
}

func (a *apiV1) Analyze(image *docker.Image) ([]*Vulnerability, error) {
url := fmt.Sprintf("%s/v1/layers/%s?vulnerabilities", a.url, image.AnalyzedLayerName())
request, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("can't create an analyze request: %s", err)
}
utils.DumpRequest(request)
response, err := a.client.Do(request)
if err != nil {
return nil, err
}
utils.DumpResponse(response)
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
body, _ := ioutil.ReadAll(response.Body)
return nil, fmt.Errorf("analyze error %d: %s", response.StatusCode, string(body))
}
var envelope layerEnvelope
if err = json.NewDecoder(response.Body).Decode(&envelope); err != nil {
return nil, err
}
var vs []*Vulnerability
for _, f := range envelope.Layer.Features {
for _, v := range f.Vulnerabilities {
v.FeatureName = f.Name
vs = append(vs, &v)
}
}
return vs, nil
}

func (a *apiV3) Push(image *docker.Image) error {
fmt.Println("grpc push image")
req := &clairpb.PostAncestryRequest{
Format: "Docker",
AncestryName: image.Name,
}

ls := make([]*clairpb.PostAncestryRequest_PostLayer, len(image.FsLayers))
for i := 0; i < len(image.FsLayers); i++ {
ls[i] = newLayerV3(image, i)
}
req.Layers = ls
resp, err := a.client.PostAncestry(context.Background(), req)
if err != nil {
return err
}
fmt.Printf("grpc resp: %v", resp)
return nil
}

func newLayerV3(image *docker.Image, index int) *clairpb.PostAncestryRequest_PostLayer {
return &clairpb.PostAncestryRequest_PostLayer{
Hash: image.LayerName(index),
Path: strings.Join([]string{image.Registry, image.Name, "blobs", image.FsLayers[index].BlobSum}, "/"),
Headers: map[string]string{"Authorization": image.Token},
}
}

func (a *apiV3) Analyze(image *docker.Image) ([]*Vulnerability, error) {
req := &clairpb.GetAncestryRequest{
AncestryName: image.Name,
WithFeatures: true,
WithVulnerabilities: true,
}

resp, err := a.client.GetAncestry(context.Background(), req)
if err != nil {
return nil, err
}
var vs []*Vulnerability
for _, f := range resp.Ancestry.Features {
for _, v := range f.Vulnerabilities {
cv := convertVulnerability(v)
cv.FeatureName = f.Name
vs = append(vs, cv)
}
}
return vs, nil
}

func convertVulnerability(cv *clairpb.Vulnerability) *Vulnerability {
return &Vulnerability{
Name: cv.Name,
NamespaceName: cv.NamespaceName,
Description: cv.Description,
Severity: cv.Severity,
Link: cv.Link,
FixedBy: cv.FixedBy,
}
}
118 changes: 20 additions & 98 deletions clair/clair.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
package clair

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
"time"

"github.com/optiopay/klar/docker"
"github.com/optiopay/klar/utils"
)

const EMPTY_LAYER_BLOB_SUM = "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"

// Clair is representation of Clair server
type Clair struct {
url string
client http.Client
url string
api API
}

type API interface {
Analyze(image *docker.Image) ([]*Vulnerability, error)
Push(image *docker.Image) error
}

type layer struct {
Expand Down Expand Up @@ -53,6 +52,7 @@ type Vulnerability struct {
Metadata map[string]interface{} `json:"Metadata,omitempty"`
FixedBy string `json:"FixedBy,omitempty"`
FixedIn []feature `json:"FixedIn,omitempty"`
FeatureName string `json:"featureName",omitempty`
}

type layerError struct {
Expand All @@ -70,18 +70,13 @@ type layerEnvelope struct {

// NewClair construct Clair entity using potentially incomplete server URL
// If protocol is missing HTTP will be used. If port is missing 6060 will be used
func NewClair(url string) Clair {
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
url = fmt.Sprintf("http://%s", url)
}
if strings.LastIndex(url, ":") < 5 {
url = fmt.Sprintf("%s:6060", url)
}
client := http.Client{
Timeout: time.Minute,
func NewClair(url string, version int) Clair {
api, err := newAPI(url, version)
if err != nil {
panic(fmt.Sprintf("cant't create API client version %d %s: %s", version, url, err))
}

return Clair{url, client}
return Clair{url, api}
}

func newLayer(image *docker.Image, index int) *layer {
Expand Down Expand Up @@ -110,96 +105,23 @@ func filterEmptyLayers(fsLayers []docker.FsLayer) (filteredLayers []docker.FsLay

// Analyse sent each layer from Docker image to Clair and returns
// a list of found vulnerabilities
func (c *Clair) Analyse(image *docker.Image) []Vulnerability {
func (c *Clair) Analyse(image *docker.Image) ([]*Vulnerability, error) {
// Filter the empty layers in image
image.FsLayers = filterEmptyLayers(image.FsLayers)
layerLength := len(image.FsLayers)
if layerLength == 0 {
fmt.Fprintf(os.Stderr, "No need to analyse image %s/%s:%s as there is no non-emtpy layer\n",
fmt.Fprintf(os.Stderr, "no need to analyse image %s/%s:%s as there is no non-emtpy layer\n",
image.Registry, image.Name, image.Tag)
return nil
return nil, nil
}

var vs []Vulnerability
for i := 0; i < layerLength; i++ {
layer := newLayer(image, i)
err := c.pushLayer(layer)
if err != nil {
fmt.Fprintf(os.Stderr, "Push layer %d failed: %s\n", i, err.Error())
continue
}
if err := c.api.Push(image); err != nil {
return nil, fmt.Errorf("push image %s/%s:%s to Clair failed: %s\n", image.Registry, image.Name, image.Tag, err.Error())
}

vs, err := c.analyzeLayer(image.AnalyzedLayerName())
vs, err := c.api.Analyze(image)
if err != nil {
fmt.Fprintf(os.Stderr, "Analyse image %s/%s:%s failed: %s\n", image.Registry, image.Name, image.Tag, err.Error())
return nil
return nil, fmt.Errorf("analyse image %s/%s:%s failed: %s\n", image.Registry, image.Name, image.Tag, err.Error())
}

return vs
}

func (c *Clair) analyzeLayer(layerName string) ([]Vulnerability, error) {
url := fmt.Sprintf("%s/v1/layers/%s?vulnerabilities", c.url, layerName)
request, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("Can't create an analyze request: %s", err)
}
utils.DumpRequest(request)
response, err := c.client.Do(request)
if err != nil {
return nil, err
}
utils.DumpResponse(response)
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
body, _ := ioutil.ReadAll(response.Body)
return nil, fmt.Errorf("Analyze error %d: %s", response.StatusCode, string(body))
}
var envelope layerEnvelope
if err = json.NewDecoder(response.Body).Decode(&envelope); err != nil {
return nil, err
}
var vs []Vulnerability
for _, f := range envelope.Layer.Features {
for _, v := range f.Vulnerabilities {
vs = append(vs, v)
}
}
return vs, nil
}

func (c *Clair) pushLayer(layer *layer) error {
envelope := layerEnvelope{Layer: layer}
reqBody, err := json.Marshal(envelope)
if err != nil {
return fmt.Errorf("can't serialze push request: %s", err)
}
url := fmt.Sprintf("%s/v1/layers", c.url)
request, err := http.NewRequest("POST", url, bytes.NewBuffer(reqBody))
if err != nil {
return fmt.Errorf("Can't create a push request: %s", err)
}
request.Header.Set("Content-Type", "application/json")
utils.DumpRequest(request)
response, err := c.client.Do(request)
if err != nil {
return fmt.Errorf("Can't push layer to Clair: %s", err)
}
utils.DumpResponse(response)
defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return fmt.Errorf("Can't read clair response : %s", err)
}
if response.StatusCode != http.StatusCreated {
var lerr layerError
err = json.Unmarshal(body, &lerr)
if err != nil {
return fmt.Errorf("Can't even read an error message: %s", err)
}
return fmt.Errorf("Push error %d: %s", response.StatusCode, string(body))
}
return nil

}
7 changes: 5 additions & 2 deletions clair/clair_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,11 @@ func TestAnalyse(t *testing.T) {
Token: imageToken,
}

c := NewClair(ts.URL)
vs := c.Analyse(dockerImage)
c := NewClair(ts.URL, 1)
vs, err := c.Analyse(dockerImage)
if err != nil {
t.Fatal(err)
}
if len(vs) != 1 {
t.Fatalf("Expected 1 vulnerability, got %d", len(vs))
}
Expand Down
Loading

0 comments on commit 9466589

Please sign in to comment.