From 73712bf067c2ab908dee78fdb4eb0822262ef799 Mon Sep 17 00:00:00 2001 From: Jess Frazelle Date: Tue, 12 Jun 2018 10:04:35 -0400 Subject: [PATCH] add clair integration tests Signed-off-by: Jess Frazelle --- Dockerfile.clair | 11 +++ main_test.go | 15 +++- registry/tokentransport.go | 3 +- testutils/configs/clair.yml | 75 ++++++++++++++++ testutils/testutils.go | 169 ++++++++++++++++++++++++++++++++++-- vulns_test.go | 18 ++++ 6 files changed, 282 insertions(+), 9 deletions(-) create mode 100644 Dockerfile.clair create mode 100644 testutils/configs/clair.yml create mode 100644 vulns_test.go diff --git a/Dockerfile.clair b/Dockerfile.clair new file mode 100644 index 00000000..29460f73 --- /dev/null +++ b/Dockerfile.clair @@ -0,0 +1,11 @@ +FROM quay.io/coreos/clair + +RUN apk add --no-cache \ + ca-certificates + +COPY testutils/snakeoil/cert.pem /usr/local/share/ca-certificates/clair.pem + +# normally we'd use update-ca-certificates, but something about running it in +# Alpine is off, and the certs don't get added. Fortunately, we only need to +# add ca-certificates to the global store and it's all plain text. +RUN cat /usr/local/share/ca-certificates/* >> /etc/ssl/certs/ca-certificates.crt diff --git a/main_test.go b/main_test.go index be68d8b1..97ffde28 100644 --- a/main_test.go +++ b/main_test.go @@ -62,11 +62,18 @@ func TestMain(m *testing.M) { panic(fmt.Errorf("could not connect to docker: %v", err)) } + // start the clair containers. + dbID, clairID, err := testutils.StartClair(dcli) + if err != nil { + testutils.RemoveContainer(dcli, dbID, clairID) + panic(fmt.Errorf("starting clair containers failed: %v", err)) + } + for _, regConfig := range registryConfigs { // start each registry regID, _, err := testutils.StartRegistry(dcli, regConfig.config, regConfig.username, regConfig.password) if err != nil { - testutils.RemoveContainer(dcli, regID) + testutils.RemoveContainer(dcli, dbID, clairID, regID) panic(fmt.Errorf("starting registry container %s failed: %v", regConfig.config, err)) } @@ -79,11 +86,17 @@ func TestMain(m *testing.M) { } if merr != 0 { + testutils.RemoveContainer(dcli, dbID, clairID) fmt.Printf("testing config %s failed\n", regConfig.config) os.Exit(merr) } } + // remove clair containers. + if err := testutils.RemoveContainer(dcli, dbID, clairID); err != nil { + log.Printf("couldn't remove clair containers: %v", err) + } + os.Exit(0) } diff --git a/registry/tokentransport.go b/registry/tokentransport.go index b33df95c..a21ccd75 100644 --- a/registry/tokentransport.go +++ b/registry/tokentransport.go @@ -198,7 +198,8 @@ func (r *Registry) Headers(uri string) (map[string]string, error) { } if len(token) < 1 { - return nil, fmt.Errorf("got empty token for %s", uri) + r.Logf("got empty token for %s", uri) + return map[string]string{}, nil } return map[string]string{ diff --git a/testutils/configs/clair.yml b/testutils/configs/clair.yml new file mode 100644 index 00000000..53bdd7ed --- /dev/null +++ b/testutils/configs/clair.yml @@ -0,0 +1,75 @@ +# Copyright 2015 clair authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# The values specified here are the default values that Clair uses if no configuration file is specified or if the keys are not defined. +clair: + database: + # Database driver + type: pgsql + options: + # PostgreSQL Connection string + # https://www.postgresql.org/docs/current/static/libpq-connect.html#LIBPQ-CONNSTRING + source: postgresql://hacker:password@127.0.0.1:5432/clair?sslmode=disable&statement_timeout=60000 + + # Number of elements kept in the cache + # Values unlikely to change (e.g. namespaces) are cached in order to save prevent needless roundtrips to the database. + cachesize: 16384 + + api: + # API server port + addr: "0.0.0.0:6060" + + # Health server port + # This is an unencrypted endpoint useful for load balancers to check to healthiness of the clair server. + healthaddr: "0.0.0.0:6061" + + # Deadline before an API request will respond with a 503 + timeout: 900s + + # Optional PKI configuration + # If you want to easily generate client certificates and CAs, try the following projects: + # https://github.com/coreos/etcd-ca + # https://github.com/cloudflare/cfssl + servername: + cafile: + keyfile: + certfile: + + updater: + # Frequency the database will be updated with vulnerabilities from the default data sources + # The value 0 disables the updater entirely. + interval: 2h + + notifier: + # Number of attempts before the notification is marked as failed to be sent + attempts: 3 + + # Duration before a failed notification is retried + renotifyinterval: 2h + + http: + # Optional endpoint that will receive notifications via POST requests + endpoint: + + # Optional PKI configuration + # If you want to easily generate client certificates and CAs, try the following projects: + # https://github.com/cloudflare/cfssl + # https://github.com/coreos/etcd-ca + servername: + cafile: + keyfile: + certfile: + + # Optional HTTP Proxy: must be a valid URL (including the scheme). + proxy: diff --git a/testutils/testutils.go b/testutils/testutils.go index 247d4e5f..93dc365f 100644 --- a/testutils/testutils.go +++ b/testutils/testutils.go @@ -1,6 +1,9 @@ package testutils import ( + "archive/tar" + "bufio" + "bytes" "context" "crypto/tls" "crypto/x509" @@ -8,6 +11,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "io/ioutil" "net" "net/http" @@ -50,6 +54,9 @@ func StartRegistry(dcli *client.Client, config, username, password string) (stri filepath.Join(filepath.Dir(filename), "configs", "htpasswd") + ":" + "/etc/docker/registry/htpasswd" + ":ro", filepath.Join(filepath.Dir(filename), "snakeoil") + ":" + "/etc/docker/registry/ssl" + ":ro", }, + RestartPolicy: container.RestartPolicy{ + Name: "always", + }, }, nil, "") if err != nil { @@ -73,7 +80,7 @@ func StartRegistry(dcli *client.Client, config, username, password string) (stri } // Prefill the images. - images := []string{"alpine:latest", "busybox:latest", "busybox:musl", "busybox:glibc"} + images := []string{"alpine:3.5", "alpine:latest", "busybox:latest", "busybox:musl", "busybox:glibc"} for _, image := range images { if err := prefillRegistry(image, dcli, "localhost"+port, username, password); err != nil { return r.ID, addr, err @@ -83,13 +90,115 @@ func StartRegistry(dcli *client.Client, config, username, password string) (stri return r.ID, addr, nil } +func startClairDB(dcli *client.Client) (string, error) { + image := "postgres:latest" + + if err := pullDockerImage(dcli, image); err != nil { + return "", err + } + + c, err := dcli.ContainerCreate( + context.Background(), + &container.Config{ + Image: image, + Env: []string{ + "POSTGRES_PASSWORD=password", + "POSTGRES_DB=clair", + "POSTGRES_USER=hacker", + }, + }, + &container.HostConfig{ + NetworkMode: "host", + RestartPolicy: container.RestartPolicy{ + Name: "always", + }, + }, + nil, "") + if err != nil { + return "", err + } + + // start the container + return c.ID, dcli.ContainerStart(context.Background(), c.ID, types.ContainerStartOptions{}) +} + +// StartClair starts a new clair container and accompanying database. +func StartClair(dcli *client.Client) (string, string, error) { + _, filename, _, ok := runtime.Caller(0) + if !ok { + return "", "", errors.New("No caller information") + } + + // start the database container. + dbID, err := startClairDB(dcli) + if err != nil { + return dbID, "", err + } + + image := "clair:dev" + + // build the docker image + // create the tar ball + ctx := filepath.Dir(filepath.Dir(filename)) + tw, err := tarit(ctx) + if err != nil { + return dbID, "", err + } + + // build the image + resp, err := dcli.ImageBuild(context.Background(), tw, types.ImageBuildOptions{ + Tags: []string{image}, + Dockerfile: "Dockerfile.clair", + ForceRemove: true, + Remove: true, + SuppressOutput: false, + PullParent: true, + }) + if err != nil { + return dbID, "", err + } + defer resp.Body.Close() + + c, err := dcli.ContainerCreate( + context.Background(), + &container.Config{ + Image: image, + }, + &container.HostConfig{ + NetworkMode: "host", + Binds: []string{ + filepath.Join(filepath.Dir(filename), "configs", "clair.yml") + ":" + "/etc/clair/config.yaml" + ":ro", + }, + RestartPolicy: container.RestartPolicy{ + Name: "always", + }, + }, + nil, "") + if err != nil { + return dbID, c.ID, err + } + + // start the container + err = dcli.ContainerStart(context.Background(), c.ID, types.ContainerStartOptions{}) + + // wait for clair to start + // TODO: make this not a sleep + time.Sleep(time.Second * 5) + + return dbID, c.ID, err +} + // RemoveContainer removes with force a container by it's container ID. -func RemoveContainer(dcli *client.Client, ctrID string) error { - return dcli.ContainerRemove(context.Background(), ctrID, - types.ContainerRemoveOptions{ - RemoveVolumes: true, - Force: true, - }) +func RemoveContainer(dcli *client.Client, ctrs ...string) (err error) { + for _, c := range ctrs { + err = dcli.ContainerRemove(context.Background(), c, + types.ContainerRemoveOptions{ + RemoveVolumes: true, + Force: true, + }) + } + + return err } // dockerLogin logins via the command line to a docker registry @@ -239,3 +348,49 @@ func constructRegistryAuth(identity, secret string) (string, error) { return base64.URLEncoding.EncodeToString(buf), nil } + +func tarit(src string) (io.Reader, error) { + s := bytes.NewBuffer(nil) + t := bytes.NewBuffer(nil) + buf := bufio.NewReadWriter(bufio.NewReader(s), bufio.NewWriter(t)) + tarball := tar.NewWriter(s) + + err := filepath.Walk(src, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + header, err := tar.FileInfoHeader(info, info.Name()) + if err != nil { + return err + } + + header.Name = strings.TrimPrefix(path, src) + + if err := tarball.WriteHeader(header); err != nil { + return err + } + + if info.IsDir() { + return nil + } + + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + + _, err = io.Copy(tarball, file) + return err + }) + if err != nil { + return nil, err + } + + if _, err := s.WriteTo(buf); err != nil { + return nil, err + } + + err = buf.Writer.Flush() + return t, err +} diff --git a/vulns_test.go b/vulns_test.go new file mode 100644 index 00000000..613c30e0 --- /dev/null +++ b/vulns_test.go @@ -0,0 +1,18 @@ +package main + +import ( + "strings" + "testing" +) + +func TestVulns(t *testing.T) { + out, err := run("vulns", "--clair", "http://localhost:6060", "alpine:3.5") + if err != nil { + t.Fatalf("output: %s, error: %v", string(out), err) + } + + expected := `clair.clair resp.Status=200 OK` + if !strings.HasSuffix(strings.TrimSpace(out), expected) { + t.Logf("expected: %s\ngot: %s", expected, out) + } +}