Skip to content

Commit

Permalink
fix(trivy): Set Registry and Artifact properties in the Vulnerability…
Browse files Browse the repository at this point in the history
…Report (#85)

Signed-off-by: Daniel Pacak <[email protected]>
  • Loading branch information
danielpacak authored Jul 9, 2020
1 parent 025678d commit 92c23ca
Show file tree
Hide file tree
Showing 8 changed files with 800 additions and 33 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/aquasecurity/starboard
go 1.14

require (
github.com/google/go-containerregistry v0.1.1
github.com/google/uuid v1.1.1
github.com/spf13/cobra v1.0.0
github.com/spf13/pflag v1.0.5
Expand Down
696 changes: 675 additions & 21 deletions go.sum

Large diffs are not rendered by default.

40 changes: 34 additions & 6 deletions pkg/find/vulnerabilities/trivy/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import (
"strings"

starboard "github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
"github.com/google/go-containerregistry/pkg/name"
)

// Converter is the interface that wraps the Convert method.
//
// Convert converts the vulnerabilities model used by Trivy
// to a generic model defined by the Custom Security Resource Specification.
type Converter interface {
Convert(reader io.Reader) (starboard.VulnerabilityReport, error)
Convert(imageRef string, reader io.Reader) (starboard.VulnerabilityReport, error)
}

type converter struct {
Expand All @@ -26,7 +27,7 @@ func NewConverter() Converter {
return &converter{}
}

func (c *converter) Convert(reader io.Reader) (report starboard.VulnerabilityReport, err error) {
func (c *converter) Convert(imageRef string, reader io.Reader) (report starboard.VulnerabilityReport, err error) {
var scanReports []ScanReport
skipReader, err := c.skippingNoisyOutputReader(reader)
if err != nil {
Expand All @@ -36,8 +37,7 @@ func (c *converter) Convert(reader io.Reader) (report starboard.VulnerabilityRep
if err != nil {
return
}
report = c.convert(scanReports)
return
return c.convert(imageRef, scanReports)
}

// TODO Normally I'd use Trivy with the --quiet flag, but in case of errors it does suppress the error message.
Expand All @@ -60,7 +60,7 @@ func (c *converter) skippingNoisyOutputReader(input io.Reader) (io.Reader, error
return strings.NewReader(inputAsString), nil
}

func (c *converter) convert(reports []ScanReport) starboard.VulnerabilityReport {
func (c *converter) convert(imageRef string, reports []ScanReport) (starboard.VulnerabilityReport, error) {
vulnerabilities := make([]starboard.VulnerabilityItem, 0)

for _, report := range reports {
Expand All @@ -79,15 +79,22 @@ func (c *converter) convert(reports []ScanReport) starboard.VulnerabilityReport
}
}

registry, artifact, err := c.parseImageRef(imageRef)
if err != nil {
return starboard.VulnerabilityReport{}, err
}

return starboard.VulnerabilityReport{
Scanner: starboard.Scanner{
Name: "Trivy",
Vendor: "Aqua Security",
Version: trivyVersion,
},
Registry: registry,
Artifact: artifact,
Summary: c.toSummary(vulnerabilities),
Vulnerabilities: vulnerabilities,
}
}, nil
}

func (c *converter) toLinks(references []string) []string {
Expand All @@ -114,3 +121,24 @@ func (c *converter) toSummary(vulnerabilities []starboard.VulnerabilityItem) (vs
}
return
}

func (c *converter) parseImageRef(imageRef string) (starboard.Registry, starboard.Artifact, error) {
ref, err := name.ParseReference(imageRef)
if err != nil {
return starboard.Registry{}, starboard.Artifact{}, err
}
registry := starboard.Registry{
URL: ref.Context().RegistryStr(),
}
artifact := starboard.Artifact{
Repository: ref.Context().RepositoryStr(),
}
switch t := ref.(type) {
case name.Tag:
artifact.Tag = t.TagStr()
case name.Digest:
artifact.Digest = t.DigestStr()
}

return registry, artifact, nil
}
31 changes: 28 additions & 3 deletions pkg/find/vulnerabilities/trivy/converter_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package trivy

import (
"errors"
"fmt"
"strings"
"testing"
Expand Down Expand Up @@ -48,6 +49,13 @@ var (
Vendor: "Aqua Security",
Version: "0.9.1",
},
Registry: starboard.Registry{
URL: "index.docker.io",
},
Artifact: starboard.Artifact{
Repository: "library/alpine",
Tag: "3.10.2",
},
Summary: starboard.VulnerabilitySummary{
CriticalCount: 0,
MediumCount: 1,
Expand Down Expand Up @@ -86,24 +94,28 @@ func TestConverter_Convert(t *testing.T) {

testCases := []struct {
name string
imageRef string
input string
expectedError error
expectedReport starboard.VulnerabilityReport
}{
{
name: "Should convert vulnerability report in JSON format when input is noisy",
name: "Should convert vulnerability report in JSON format when input is noisy",
imageRef: "alpine:3.10.2",
input: fmt.Sprintf("2020-06-17T23:37:45.320+0200 INFO Detecting Alpine vulnerabilities...\n%s", sampleReportAsString),
expectedError: nil,
expectedReport: sampleReport,
},
{
name: "Should convert vulnerability report in JSON format when input is quiet",
imageRef: "alpine:3.10.2",
input: sampleReportAsString,
expectedError: nil,
expectedReport: sampleReport,
},
{
name: "Should convert vulnerability report in JSON format when OS is not detected",
name: "Should convert vulnerability report in JSON format when OS is not detected",
imageRef: "core.harbor.domain/library/nginx@sha256:d20aa6d1cae56fd17cd458f4807e0de462caf2336f0b70b5eeb69fcaaf30dd9c",
input: `2020-06-21T23:10:15.162+0200 WARN OS is not detected and vulnerabilities in OS packages are not detected.
null`,
expectedError: nil,
Expand All @@ -113,6 +125,13 @@ null`,
Vendor: "Aqua Security",
Version: "0.9.1",
},
Registry: starboard.Registry{
URL: "core.harbor.domain",
},
Artifact: starboard.Artifact{
Repository: "library/nginx",
Digest: "sha256:d20aa6d1cae56fd17cd458f4807e0de462caf2336f0b70b5eeb69fcaaf30dd9c",
},
Summary: starboard.VulnerabilitySummary{
CriticalCount: 0,
HighCount: 0,
Expand All @@ -124,11 +143,17 @@ null`,
Vulnerabilities: []starboard.VulnerabilityItem{},
},
},
{
name: "Should return error when image reference cannot be parsed",
imageRef: ":",
input: "null",
expectedError: errors.New("could not parse reference: :"),
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
report, err := NewConverter().Convert(strings.NewReader(tc.input))
report, err := NewConverter().Convert(tc.imageRef, strings.NewReader(tc.input))
switch {
case tc.expectedError == nil:
require.NoError(t, err)
Expand Down
32 changes: 30 additions & 2 deletions pkg/find/vulnerabilities/trivy/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ package trivy
import (
"context"
"fmt"
"github.com/aquasecurity/starboard/pkg/scanners"
"io"

"github.com/aquasecurity/starboard/pkg/scanners"
"k8s.io/klog"

"github.com/aquasecurity/starboard/pkg/kube"
Expand Down Expand Up @@ -132,8 +133,12 @@ func (s *Scanner) PrepareScanJob(ctx context.Context, workload kube.Object, spec
},
}

containerImages := kube.ContainerImages{}

scanJobContainers := make([]core.Container, len(spec.Containers))
for i, c := range spec.Containers {
containerImages[c.Name] = c.Image

var envs []core.EnvVar
if dockerConfig, ok := credentials[c.Image]; ok {
envs = append(envs, core.EnvVar{
Expand Down Expand Up @@ -173,6 +178,11 @@ func (s *Scanner) PrepareScanJob(ctx context.Context, workload kube.Object, spec
}
}

containerImagesAsJSON, err := containerImages.AsJSON()
if err != nil {
return nil, err
}

return &batch.Job{
ObjectMeta: meta.ObjectMeta{
Name: jobName,
Expand All @@ -182,6 +192,9 @@ func (s *Scanner) PrepareScanJob(ctx context.Context, workload kube.Object, spec
kube.LabelResourceName: workload.Name,
kube.LabelResourceNamespace: workload.Namespace,
},
Annotations: map[string]string{
kube.AnnotationContainerImages: containerImagesAsJSON,
},
},
Spec: batch.JobSpec{
BackoffLimit: pointer.Int32Ptr(0),
Expand Down Expand Up @@ -219,14 +232,29 @@ func (s *Scanner) PrepareScanJob(ctx context.Context, workload kube.Object, spec
func (s *Scanner) GetVulnerabilityReportsByScanJob(ctx context.Context, job *batch.Job) (reports vulnerabilities.WorkloadVulnerabilities, err error) {
reports = make(map[string]sec.VulnerabilityReport)

var containerImagesAsJSON string
var ok bool

if containerImagesAsJSON, ok = job.Annotations[kube.AnnotationContainerImages]; !ok {
err = fmt.Errorf("scan job does not have required annotation: %s", kube.AnnotationContainerImages)
return

}
containerImages := kube.ContainerImages{}
err = containerImages.FromJSON(containerImagesAsJSON)
if err != nil {
err = fmt.Errorf("reading scan job annotation: %s: %w", kube.AnnotationContainerImages, err)
return
}

for _, c := range job.Spec.Template.Spec.Containers {
klog.V(3).Infof("Getting logs for %s container in job: %s/%s", c.Name, job.Namespace, job.Name)
var logReader io.ReadCloser
logReader, err = s.pods.GetContainerLogsByJob(ctx, job, c.Name)
if err != nil {
return
}
reports[c.Name], err = s.converter.Convert(logReader)
reports[c.Name], err = s.converter.Convert(containerImages[c.Name], logReader)
_ = logReader.Close()
if err != nil {
return
Expand Down
16 changes: 16 additions & 0 deletions pkg/kube/object.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package kube

import (
"encoding/json"
"fmt"

"k8s.io/apimachinery/pkg/labels"
Expand Down Expand Up @@ -68,3 +69,18 @@ func KindFromResource(resource string) (Kind, error) {
}
return KindUnknown, fmt.Errorf("unrecognized resource: %s", resource)
}

// ContainerImages is a simple structure to hold the mapping between container names and container image references.
type ContainerImages map[string]string

func (ci ContainerImages) AsJSON() (string, error) {
writer, err := json.Marshal(ci)
if err != nil {
return "", err
}
return string(writer), nil
}

func (ci ContainerImages) FromJSON(value string) error {
return json.Unmarshal([]byte(value), &ci)
}
14 changes: 14 additions & 0 deletions pkg/kube/object_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,17 @@ func TestObjectFromLabelsSet(t *testing.T) {
})
}
}

func TestContainerImages_AsJSON_And_FromJSON(t *testing.T) {
containerImages := ContainerImages{
"nginx": "nginx:1.16",
"redis": "core.harbor.domain:8443/library/redis:5",
}
value, err := containerImages.AsJSON()
require.NoError(t, err)

newContainerImages := ContainerImages{}
err = newContainerImages.FromJSON(value)
require.NoError(t, err)
assert.Equal(t, containerImages, newContainerImages)
}
3 changes: 2 additions & 1 deletion pkg/kube/starboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ const (
)

const (
AnnotationHistoryLimit = "starboard.history.limit"
AnnotationHistoryLimit = "starboard.history.limit"
AnnotationContainerImages = "starboard.container-images"
)

// ScannerOpts holds configuration of the vulnerability Scanner.
Expand Down

0 comments on commit 92c23ca

Please sign in to comment.