Skip to content

Commit

Permalink
fix: Find vulnerabilities in workloads running images from private Do…
Browse files Browse the repository at this point in the history
…ckerHub repositories (#139)

Signed-off-by: Daniel Pacak <[email protected]>
  • Loading branch information
danielpacak authored Aug 31, 2020
1 parent b2ed495 commit 6169a02
Show file tree
Hide file tree
Showing 9 changed files with 383 additions and 67 deletions.
109 changes: 109 additions & 0 deletions itest/starboard_cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package itest

import (
"context"
"os"
"time"

"github.com/aquasecurity/starboard/pkg/kube/secrets"

"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"

. "github.com/onsi/gomega/gbytes"
Expand Down Expand Up @@ -329,6 +332,112 @@ var _ = Describe("Starboard CLI", func() {

})

// TODO Run with other integration tests
// The only reason this test is marked as pending is that I don't know
// how to pass DockerHub private repository credentials to this test case.
PContext("when unmanaged Pod with private image is specified as workload", func() {
var pod *corev1.Pod

var podName = "nginx-with-private-image"
var secretName = "registry-credentials"
var podNamespace = corev1.NamespaceDefault

BeforeEach(func() {
var err error
var secret *corev1.Secret
secret, err = secrets.NewImagePullSecret(metav1.ObjectMeta{
Name: secretName,
Namespace: podNamespace,
}, "https://index.docker.io/v1",
os.Getenv("STARBOARD_TEST_DOCKERHUB_REGISTRY_USERNAME"),
os.Getenv("STARBOARD_TEST_DOCKERHUB_REGISTRY_PASSWORD"))
Expect(err).ToNot(HaveOccurred())

_, err = kubernetesClientset.CoreV1().Secrets(podNamespace).
Create(context.TODO(), secret, metav1.CreateOptions{})
Expect(err).ToNot(HaveOccurred())

pod, err = kubernetesClientset.CoreV1().Pods(podNamespace).
Create(context.TODO(), &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: podName,
Namespace: podNamespace,
},
Spec: corev1.PodSpec{
ImagePullSecrets: []corev1.LocalObjectReference{
{
Name: secretName,
},
},
Containers: []corev1.Container{
{
Name: "nginx",
Image: "starboardcicd/private-nginx:1.16",
ImagePullPolicy: corev1.PullAlways,
},
},
},
}, metav1.CreateOptions{})
Expect(err).ToNot(HaveOccurred())
})

It("should create vulnerabilities resources", func() {
err := cmd.Run(versionInfo, []string{
"starboard",
"find", "vulnerabilities", "po/nginx-with-private-image",
"-v", starboardCLILogLevel,
}, GinkgoWriter, GinkgoWriter)
Expect(err).ToNot(HaveOccurred())

reportList, err := starboardClientset.AquasecurityV1alpha1().Vulnerabilities(podNamespace).
List(context.TODO(), metav1.ListOptions{
LabelSelector: labels.Set{
kube.LabelResourceKind: "Pod",
kube.LabelResourceName: podName,
kube.LabelResourceNamespace: podNamespace,
}.String(),
})
Expect(err).ToNot(HaveOccurred())

Expect(reportList.Items).To(MatchAllElements(containerNameAsIdFn, Elements{
"nginx": MatchFields(IgnoreExtras, Fields{
"ObjectMeta": MatchFields(IgnoreExtras, Fields{
"Labels": MatchAllKeys(Keys{
kube.LabelContainerName: Equal("nginx"),
kube.LabelResourceKind: Equal("Pod"),
kube.LabelResourceName: Equal(podName),
kube.LabelResourceNamespace: Equal(podNamespace),
}),
"OwnerReferences": ConsistOf(metav1.OwnerReference{
APIVersion: "v1",
Kind: "Pod",
Name: podName,
UID: pod.UID,
}),
}),
"Report": MatchFields(IgnoreExtras, Fields{
"Scanner": Equal(v1alpha1.Scanner{
Name: "Trivy",
Vendor: "Aqua Security",
Version: "0.9.1",
}),
}),
}),
}))
})

AfterEach(func() {
err := kubernetesClientset.CoreV1().Pods(podNamespace).
Delete(context.TODO(), podName, metav1.DeleteOptions{})
Expect(err).ToNot(HaveOccurred())

err = kubernetesClientset.CoreV1().Secrets(podNamespace).
Delete(context.TODO(), secretName, metav1.DeleteOptions{})
Expect(err).ToNot(HaveOccurred())
})

})

Context("when ReplicaSet is specified as workload", func() {
var rs *appsv1.ReplicaSet
var rsName = "nginx"
Expand Down
122 changes: 97 additions & 25 deletions pkg/docker/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,119 @@ package docker
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"strings"

"github.com/google/go-containerregistry/pkg/name"
)

// ServerCredentials represent credentials used to login to a Docker server.
type ServerCredentials struct {
Auth string `json:"auth"`
Username string `json:"username"`
Password string `json:"password"`
type BasicAuth string

func NewBasicAuth(username, password string) BasicAuth {
var v = new(BasicAuth)
v.Encode(username, password)
return *v
}

// Credentials represents Docker credentials which are typically stored in `~/.docker/config.json`.
type Credentials struct {
Auths map[string]ServerCredentials `json:"auths"`
func (v *BasicAuth) Encode(username, password string) {
*v = BasicAuth(base64.StdEncoding.EncodeToString(
[]byte(fmt.Sprintf("%s:%s", username, password))))
}

func ReadCredentialsFromBytes(contents []byte) (cfg map[string]ServerCredentials, err error) {
var credentials Credentials
if err = json.Unmarshal(contents, &credentials); err != nil {
return nil, err
func (v *BasicAuth) Decode() (username, password string, err error) {
bytes, err := base64.StdEncoding.DecodeString(string(*v))
if err != nil {
return
}
return encodeAuth(credentials.Auths)
split := strings.Split(string(bytes), ":")

username = split[0]
password = split[1]
return
}

func (v BasicAuth) String() string {
return "[REDACTED]"
}

// Auth represent credentials used to login to a Docker registry.
type Auth struct {
Auth BasicAuth `json:"auth,omitempty"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
}

func (v Auth) String() string {
return "[REDACTED]"
}

// Config represents Docker configuration which is typically saved as `~/.docker/config.json`.
type Config struct {
Auths map[string]Auth `json:"auths"`
}

func (c *Config) Read(contents []byte) (err error) {
if err := json.Unmarshal(contents, c); err != nil {
return err
}
c.Auths, err = decodeAuths(c.Auths)
return
}

func encodeAuth(config map[string]ServerCredentials) (encodedConfig map[string]ServerCredentials, err error) {
encodedConfig = make(map[string]ServerCredentials)
for server, entry := range config {
if (ServerCredentials{}) == entry {
func decodeAuths(auths map[string]Auth) (map[string]Auth, error) {
decodedAuths := make(map[string]Auth)
for server, entry := range auths {
if (Auth{}) == entry {
continue
}
var decodedAuth []byte
decodedAuth, err = base64.StdEncoding.DecodeString(entry.Auth)

username, password, err := entry.Auth.Decode()
if err != nil {
return
return nil, err
}
splitDecodedAuth := strings.Split(string(decodedAuth), ":")
encodedConfig[server] = ServerCredentials{

decodedAuths[server] = Auth{
Auth: entry.Auth,
Username: splitDecodedAuth[0],
Password: splitDecodedAuth[1],
Username: username,
Password: password,
}

}
return
return decodedAuths, nil
}

func (c Config) Write() ([]byte, error) {
bytes, err := json.Marshal(&c)
if err != nil {
return nil, err
}
return bytes, nil
}

// GetServerFromImageRef returns registry server from the specified imageRef.
func GetServerFromImageRef(imageRef string) (string, error) {
ref, err := name.ParseReference(imageRef)
if err != nil {
return "", err
}
return ref.Context().RegistryStr(), nil
}

// GetHostFromServer returns the host for the specified registry server.
//
// In ~/.docker/config.json auth keys can be specified as URLs or host names.
// For the sake of comparison we need to normalize the registry identifier.
func GetHostFromServer(server string) (string, error) {
if strings.HasPrefix(server, "http://") ||
strings.HasPrefix(server, "https://") {

parsed, err := url.Parse(server)
if err != nil {
return "", err
}

return parsed.Host, nil
}
return server, nil
}
76 changes: 62 additions & 14 deletions pkg/docker/config_test.go
Original file line number Diff line number Diff line change
@@ -1,30 +1,36 @@
package docker
package docker_test

import (
"errors"
"testing"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"

"github.com/aquasecurity/starboard/pkg/docker"
. "github.com/onsi/ginkgo/extensions/table"
"github.com/stretchr/testify/assert"
)

func TestReadCredentialsFromBytes(t *testing.T) {
// TODO Refactor to Ginkgo+Gomega
func TestConfig_Read(t *testing.T) {
testCases := []struct {
name string

givenJSON string

expectedCredentials map[string]ServerCredentials
expectedError error
expectedAuth map[string]docker.Auth
expectedError error
}{
{
name: "Should return empty credentials when content is empty JSON object",
givenJSON: "{}",
expectedCredentials: map[string]ServerCredentials{},
name: "Should return empty credentials when content is empty JSON object",
givenJSON: "{}",
expectedAuth: map[string]docker.Auth{},
},
{
name: "Should return empty credentials when content is null JSON",
givenJSON: "null",
expectedCredentials: map[string]ServerCredentials{},
name: "Should return empty credentials when content is null JSON",
givenJSON: "null",
expectedAuth: map[string]docker.Auth{},
},
{
name: "Should return error when content is blank",
Expand All @@ -43,7 +49,7 @@ func TestReadCredentialsFromBytes(t *testing.T) {
}
}
}`,
expectedCredentials: map[string]ServerCredentials{
expectedAuth: map[string]docker.Auth{
"harbor.domain": {
Auth: "YWRtaW46SGFyYm9yMTIzNDU=",
Username: "admin",
Expand All @@ -68,7 +74,7 @@ func TestReadCredentialsFromBytes(t *testing.T) {
}
}
}`,
expectedCredentials: map[string]ServerCredentials{
expectedAuth: map[string]docker.Auth{
"harbor.domain": {
Auth: "YWRtaW46SGFyYm9yMTIzNDU=",
Username: "admin",
Expand All @@ -80,15 +86,57 @@ func TestReadCredentialsFromBytes(t *testing.T) {

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
credentials, err := ReadCredentialsFromBytes([]byte(tc.givenJSON))
dockerConfig := &docker.Config{}
err := dockerConfig.Read([]byte(tc.givenJSON))
switch {
case tc.expectedError != nil:
assert.EqualError(t, err, tc.expectedError.Error())
default:
assert.NoError(t, err)
assert.Equal(t, tc.expectedCredentials, credentials)
assert.Equal(t, tc.expectedAuth, dockerConfig.Auths)
}
})

}
}

func TestDocker(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Docker")
}

var _ = Describe("Docker", func() {

DescribeTable("GetHostFromServer", func(server, expectedHost string) {
host, err := docker.GetHostFromServer(server)
Expect(err).ToNot(HaveOccurred())
Expect(host).To(Equal(expectedHost))
},
Entry("34.86.43.130.80",
"34.86.43.130.80", "34.86.43.130.80"),
Entry("core.harbor.domain:8080",
"core.harbor.domain:8080", "core.harbor.domain:8080"),
Entry("registry.aquasec.com",
"registry.aquasec.com", "registry.aquasec.com"),
Entry("https://index.docker.io/v1/",
"https://index.docker.io/v1/", "index.docker.io"),
Entry("https://registry:3780/",
"https://registry:3780/", "registry:3780"),
)

DescribeTable("GetServerFromImageRef", func(imageRef, expectedServer string) {
server, err := docker.GetServerFromImageRef(imageRef)
Expect(err).ToNot(HaveOccurred())
Expect(server).To(Equal(expectedServer))
},
Entry("aquasec/trivy:0.10.0",
"aquasec/trivy:0.10.0", "index.docker.io"),
Entry("docker.io/aquasec/harbor-scanner-trivy:0.10.0",
"docker.io/aquasec/harbor-scanner-trivy:0.10.0", "index.docker.io"),
Entry("index.docker.io/aquasec/harbor-scanner-trivy:0.10.0",
"index.docker.io/aquasec/harbor-scanner-trivy:0.10.0", "index.docker.io"),
Entry("gcr.io/google-samples/hello-app:1.0",
"gcr.io/google-samples/hello-app:1.0", "gcr.io"),
)

})
Loading

0 comments on commit 6169a02

Please sign in to comment.