Repository: aquasecurity/starboard
Pull request #591 (2021-05-20 21:29:53)

fix: Add updateTimestamp property to Open API spec of VulnerabilityReport
Signed-off-by: Daniel Pacak <pacak.daniel@gmail.com>

codecov[bot]: # [Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/591?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity) Report
> Merging [#591](https://codecov.io/gh/aquasecurity/starboard/pull/591?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity) (d3c71a3) into [main](https://codecov.io/gh/aquasecurity/starboard/commit/f500bd4c93aa6ae2e5c460970229339e319e3f7f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity) (f500bd4) will **decrease** coverage by `25.17%`.
> The diff coverage is `81.94%`.

[![Impacted file tree graph](https://codecov.io/gh/aquasecurity/starboard/pull/591/graphs/tree.svg?width=650&height=150&src=pr&token=qqTGKAJ0Wb&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity)](https://codecov.io/gh/aquasecurity/starboard/pull/591?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity)

```diff
@@             Coverage Diff             @@
##             main     #591       +/-   ##
===========================================
- Coverage   71.17%   46.00%   -25.18%     
===========================================
  Files          58       41       -17     
  Lines        4152     3500      -652     
===========================================
- Hits         2955     1610     -1345     
- Misses        871     1795      +924     
+ Partials      326       95      -231     
```


| [Impacted Files](https://codecov.io/gh/aquasecurity/starboard/pull/591?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity) | Coverage Δ | |
|---|---|---|
| [pkg/plugin/factory.go](https://codecov.io/gh/aquasecurity/starboard/pull/591/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL3BsdWdpbi9mYWN0b3J5Lmdv) | `0.00% <0.00%> (-77.15%)` | :arrow_down: |
| [pkg/plugin/trivy/plugin.go](https://codecov.io/gh/aquasecurity/starboard/pull/591/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL3BsdWdpbi90cml2eS9wbHVnaW4uZ28=) | `84.10% <83.09%> (+0.09%)` | :arrow_up: |
| [pkg/kubehunter/scanner.go](https://codecov.io/gh/aquasecurity/starboard/pull/591/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL2t1YmVodW50ZXIvc2Nhbm5lci5nbw==) | `0.00% <0.00%> (-82.41%)` | :arrow_down: |
| [pkg/vulnerabilityreport/scanner.go](https://codecov.io/gh/aquasecurity/starboard/pull/591/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL3Z1bG5lcmFiaWxpdHlyZXBvcnQvc2Nhbm5lci5nbw==) | `0.00% <0.00%> (-73.53%)` | :arrow_down: |
| [pkg/configauditreport/scanner.go](https://codecov.io/gh/aquasecurity/starboard/pull/591/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL2NvbmZpZ2F1ZGl0cmVwb3J0L3NjYW5uZXIuZ28=) | `0.00% <0.00%> (-73.22%)` | :arrow_down: |
| [pkg/operator/controller/vulnerabilityreport.go](https://codecov.io/gh/aquasecurity/starboard/pull/591/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL29wZXJhdG9yL2NvbnRyb2xsZXIvdnVsbmVyYWJpbGl0eXJlcG9ydC5nbw==) | `0.00% <0.00%> (-69.24%)` | :arrow_down: |
| [pkg/operator/controller/configauditreport.go](https://codecov.io/gh/aquasecurity/starboard/pull/591/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL29wZXJhdG9yL2NvbnRyb2xsZXIvY29uZmlnYXVkaXRyZXBvcnQuZ28=) | `0.00% <0.00%> (-65.39%)` | :arrow_down: |
| [pkg/kube/runnable\_job.go](https://codecov.io/gh/aquasecurity/starboard/pull/591/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL2t1YmUvcnVubmFibGVfam9iLmdv) | `0.00% <0.00%> (-64.39%)` | :arrow_down: |
| [pkg/operator/controller/ciskubebenchreport.go](https://codecov.io/gh/aquasecurity/starboard/pull/591/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL29wZXJhdG9yL2NvbnRyb2xsZXIvY2lza3ViZWJlbmNocmVwb3J0Lmdv) | `0.00% <0.00%> (-62.23%)` | :arrow_down: |
| ... and [29 more](https://codecov.io/gh/aquasecurity/starboard/pull/591/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/591?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/591?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity). Last update [f500bd4...d3c71a3](https://codecov.io/gh/aquasecurity/starboard/pull/591?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity).


Patch:
diff --git a/deploy/crd/vulnerabilityreports.crd.yaml b/deploy/crd/vulnerabilityreports.crd.yaml
index a293ce0..8358eae 100644
--- a/deploy/crd/vulnerabilityreports.crd.yaml
+++ b/deploy/crd/vulnerabilityreports.crd.yaml
@@ -31,11 +31,15 @@ spec:
             report:
               type: object
               required:
+                - updateTimestamp
                 - scanner
                 - artifact
                 - summary
                 - vulnerabilities
               properties:
+                updateTimestamp:
+                  type: string
+                  format: date-time
                 scanner:
                   type: object
                   required:
diff --git a/pkg/plugin/factory.go b/pkg/plugin/factory.go
index dec0895..bf4824e 100644
--- a/pkg/plugin/factory.go
+++ b/pkg/plugin/factory.go
@@ -66,7 +66,7 @@ func (r *Resolver) GetVulnerabilityPlugin() (vulnerabilityreport.Plugin, error)
 	}
 	switch scanner {
 	case starboard.Trivy:
-		return trivy.NewPlugin(ext.NewGoogleUUIDGenerator(), r.config), nil
+		return trivy.NewPlugin(ext.NewSystemClock(), ext.NewGoogleUUIDGenerator(), r.config), nil
 	case starboard.Aqua:
 		return aqua.NewPlugin(ext.NewGoogleUUIDGenerator(), r.buildInfo, r.config), nil
 	}
diff --git a/pkg/plugin/trivy/converter.go b/pkg/plugin/trivy/converter.go
deleted file mode 100644
index c9cd676..0000000
--- a/pkg/plugin/trivy/converter.go
+++ /dev/null
@@ -1,145 +0,0 @@
-package trivy
-
-import (
-	"encoding/json"
-	"io"
-	"time"
-
-	"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
-	"github.com/aquasecurity/starboard/pkg/starboard"
-	"github.com/google/go-containerregistry/pkg/name"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-)
-
-// 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(imageRef string, reader io.Reader) (v1alpha1.VulnerabilityScanResult, error)
-}
-
-type converter struct {
-	config Config
-}
-
-func NewConverter(config Config) Converter {
-	return &converter{
-		config: config,
-	}
-}
-
-func (c *converter) Convert(imageRef string, reader io.Reader) (v1alpha1.VulnerabilityScanResult, error) {
-	var scanReports []ScanReport
-	err := json.NewDecoder(reader).Decode(&scanReports)
-	if err != nil {
-		return v1alpha1.VulnerabilityScanResult{}, err
-	}
-	return c.convert(imageRef, scanReports)
-}
-
-func (c *converter) convert(imageRef string, reports []ScanReport) (v1alpha1.VulnerabilityScanResult, error) {
-	vulnerabilities := make([]v1alpha1.Vulnerability, 0)
-
-	for _, report := range reports {
-		for _, sr := range report.Vulnerabilities {
-			vulnerabilities = append(vulnerabilities, v1alpha1.Vulnerability{
-				VulnerabilityID:  sr.VulnerabilityID,
-				Resource:         sr.PkgName,
-				InstalledVersion: sr.InstalledVersion,
-				FixedVersion:     sr.FixedVersion,
-				Severity:         sr.Severity,
-				Title:            sr.Title,
-				PrimaryLink:      sr.PrimaryURL,
-				Links:            []string{},
-				Score:            GetScoreFromCvss(sr.Cvss),
-			})
-		}
-	}
-
-	registry, artifact, err := c.parseImageRef(imageRef)
-	if err != nil {
-		return v1alpha1.VulnerabilityScanResult{}, err
-	}
-
-	trivyImageRef, err := c.config.GetTrivyImageRef()
-	if err != nil {
-		return v1alpha1.VulnerabilityScanResult{}, err
-	}
-
-	version, err := starboard.GetVersionFromImageRef(trivyImageRef)
-	if err != nil {
-		return v1alpha1.VulnerabilityScanResult{}, err
-	}
-
-	return v1alpha1.VulnerabilityScanResult{
-		UpdateTimestamp: metav1.NewTime(time.Now()),
-		Scanner: v1alpha1.Scanner{
-			Name:    "Trivy",
-			Vendor:  "Aqua Security",
-			Version: version,
-		},
-		Registry:        registry,
-		Artifact:        artifact,
-		Summary:         c.toSummary(vulnerabilities),
-		Vulnerabilities: vulnerabilities,
-	}, nil
-}
-
-func (c *converter) toSummary(vulnerabilities []v1alpha1.Vulnerability) (vs v1alpha1.VulnerabilitySummary) {
-	for _, v := range vulnerabilities {
-		switch v.Severity {
-		case v1alpha1.SeverityCritical:
-			vs.CriticalCount++
-		case v1alpha1.SeverityHigh:
-			vs.HighCount++
-		case v1alpha1.SeverityMedium:
-			vs.MediumCount++
-		case v1alpha1.SeverityLow:
-			vs.LowCount++
-		default:
-			vs.UnknownCount++
-		}
-	}
-	return
-}
-
-// TODO check if it works if both tag and digest are specified
-func (c *converter) parseImageRef(imageRef string) (v1alpha1.Registry, v1alpha1.Artifact, error) {
-	ref, err := name.ParseReference(imageRef)
-	if err != nil {
-		return v1alpha1.Registry{}, v1alpha1.Artifact{}, err
-	}
-	registry := v1alpha1.Registry{
-		Server: ref.Context().RegistryStr(),
-	}
-	artifact := v1alpha1.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
-}
-
-func GetScoreFromCvss(CVSSs map[string]*CVSS) *float64 {
-	var nvdScore, vendorScore *float64
-
-	for name, cvss := range CVSSs {
-		if name == "nvd" {
-			nvdScore = cvss.V3Score
-		} else {
-			vendorScore = cvss.V3Score
-		}
-	}
-
-	if vendorScore != nil {
-		return vendorScore
-	}
-
-	return nvdScore
-}
diff --git a/pkg/plugin/trivy/plugin.go b/pkg/plugin/trivy/plugin.go
index 56d0c86..d87c64a 100644
--- a/pkg/plugin/trivy/plugin.go
+++ b/pkg/plugin/trivy/plugin.go
@@ -1,6 +1,7 @@
 package trivy
 
 import (
+	"encoding/json"
 	"fmt"
 	"io"
 
@@ -30,10 +31,11 @@ var (
 	}
 )
 
+// TODO Rename scanner struct to plugin.
 type scanner struct {
+	clock       ext.Clock
 	idGenerator ext.IDGenerator
 	config      Config
-	converter   Converter
 }
 
 // Config defines configuration params for the Trivy vulnerabilityreport.Plugin.
@@ -53,11 +55,11 @@ type Config interface {
 //
 // The starboard.ClientServer mode is usually more performant, however it
 // requires a Trivy server accessible at the configurable URL.
-func NewPlugin(idGenerator ext.IDGenerator, config Config) vulnerabilityreport.Plugin {
+func NewPlugin(clock ext.Clock, idGenerator ext.IDGenerator, config Config) vulnerabilityreport.Plugin {
 	return &scanner{
+		clock:       clock,
 		idGenerator: idGenerator,
 		config:      config,
-		converter:   NewConverter(config),
 	}
 }
 
@@ -660,14 +662,6 @@ func (s *scanner) getPodSpecForClientServerMode(spec corev1.PodSpec, credentials
 	}, secrets, nil
 }
 
-func (s *scanner) ParseVulnerabilityScanResult(imageRef string, logsReader io.ReadCloser) (v1alpha1.VulnerabilityScanResult, error) {
-	result, err := s.converter.Convert(imageRef, logsReader)
-	if err != nil {
-		return v1alpha1.VulnerabilityScanResult{}, err
-	}
-	return result, nil
-}
-
 func (s *scanner) appendTrivyInsecureEnv(image string, env []corev1.EnvVar) ([]corev1.EnvVar, error) {
 	ref, err := name.ParseReference(image)
 	if err != nil {
@@ -684,3 +678,114 @@ func (s *scanner) appendTrivyInsecureEnv(image string, env []corev1.EnvVar) ([]c
 
 	return env, nil
 }
+
+func (s *scanner) ParseVulnerabilityScanResult(imageRef string, logsReader io.ReadCloser) (v1alpha1.VulnerabilityScanResult, error) {
+	var reports []ScanReport
+	err := json.NewDecoder(logsReader).Decode(&reports)
+	if err != nil {
+		return v1alpha1.VulnerabilityScanResult{}, err
+	}
+	vulnerabilities := make([]v1alpha1.Vulnerability, 0)
+
+	for _, report := range reports {
+		for _, sr := range report.Vulnerabilities {
+			vulnerabilities = append(vulnerabilities, v1alpha1.Vulnerability{
+				VulnerabilityID:  sr.VulnerabilityID,
+				Resource:         sr.PkgName,
+				InstalledVersion: sr.InstalledVersion,
+				FixedVersion:     sr.FixedVersion,
+				Severity:         sr.Severity,
+				Title:            sr.Title,
+				PrimaryLink:      sr.PrimaryURL,
+				Links:            []string{},
+				Score:            GetScoreFromCVSS(sr.Cvss),
+			})
+		}
+	}
+
+	registry, artifact, err := s.parseImageRef(imageRef)
+	if err != nil {
+		return v1alpha1.VulnerabilityScanResult{}, err
+	}
+
+	trivyImageRef, err := s.config.GetTrivyImageRef()
+	if err != nil {
+		return v1alpha1.VulnerabilityScanResult{}, err
+	}
+
+	version, err := starboard.GetVersionFromImageRef(trivyImageRef)
+	if err != nil {
+		return v1alpha1.VulnerabilityScanResult{}, err
+	}
+
+	return v1alpha1.VulnerabilityScanResult{
+		UpdateTimestamp: metav1.NewTime(s.clock.Now()),
+		Scanner: v1alpha1.Scanner{
+			Name:    "Trivy",
+			Vendor:  "Aqua Security",
+			Version: version,
+		},
+		Registry:        registry,
+		Artifact:        artifact,
+		Summary:         s.toSummary(vulnerabilities),
+		Vulnerabilities: vulnerabilities,
+	}, nil
+}
+
+func (s *scanner) toSummary(vulnerabilities []v1alpha1.Vulnerability) (vs v1alpha1.VulnerabilitySummary) {
+	for _, v := range vulnerabilities {
+		switch v.Severity {
+		case v1alpha1.SeverityCritical:
+			vs.CriticalCount++
+		case v1alpha1.SeverityHigh:
+			vs.HighCount++
+		case v1alpha1.SeverityMedium:
+			vs.MediumCount++
+		case v1alpha1.SeverityLow:
+			vs.LowCount++
+		default:
+			vs.UnknownCount++
+		}
+	}
+	return
+}
+
+// TODO check if it works if both tag and digest are specified
+func (s *scanner) parseImageRef(imageRef string) (v1alpha1.Registry, v1alpha1.Artifact, error) {
+	ref, err := name.ParseReference(imageRef)
+	if err != nil {
+		return v1alpha1.Registry{}, v1alpha1.Artifact{}, err
+	}
+	registry := v1alpha1.Registry{
+		Server: ref.Context().RegistryStr(),
+	}
+	artifact := v1alpha1.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
+}
+
+func GetScoreFromCVSS(CVSSs map[string]*CVSS) *float64 {
+	var nvdScore, vendorScore *float64
+
+	for name, cvss := range CVSSs {
+		if name == "nvd" {
+			nvdScore = cvss.V3Score
+		} else {
+			vendorScore = cvss.V3Score
+		}
+	}
+
+	if vendorScore != nil {
+		return vendorScore
+	}
+
+	return nvdScore
+}


Test patch:
diff --git a/pkg/plugin/trivy/converter_test.go b/pkg/plugin/trivy/converter_test.go
deleted file mode 100644
index ba94f85..0000000
--- a/pkg/plugin/trivy/converter_test.go
+++ /dev/null
@@ -1,234 +0,0 @@
-package trivy_test
-
-import (
-	"errors"
-	"strings"
-	"testing"
-	"time"
-
-	"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
-	"github.com/aquasecurity/starboard/pkg/plugin/trivy"
-	"github.com/aquasecurity/starboard/pkg/starboard"
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/utils/pointer"
-)
-
-var (
-	sampleReportAsString = `[{
-		"Target": "alpine:3.10.2 (alpine 3.10.2)",
-		"Type": "alpine",
-		"Vulnerabilities": [
-			{
-				"VulnerabilityID": "CVE-2019-1549",
-				"PkgName": "openssl",
-				"InstalledVersion": "1.1.1c-r0",
-				"FixedVersion": "1.1.1d-r0",
-				"Title": "openssl: information disclosure in fork()",
-				"Description": "Usually this long long description of CVE-2019-1549",
-				"Severity": "MEDIUM",
-				"PrimaryURL": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1549",
-				"References": [
-					"https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1549"
-				]
-			},
-			{
-				"VulnerabilityID": "CVE-2019-1547",
-				"PkgName": "openssl",
-				"InstalledVersion": "1.1.1c-r0",
-				"FixedVersion": "1.1.1d-r0",
-				"Title": "openssl: side-channel weak encryption vulnerability",
-				"Severity": "LOW",
-				"PrimaryURL": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1547",
-				"References": [
-					"https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1547"
-				]
-			}
-		]
-	}]`
-
-	sampleReport = v1alpha1.VulnerabilityScanResult{
-		Scanner: v1alpha1.Scanner{
-			Name:    "Trivy",
-			Vendor:  "Aqua Security",
-			Version: "0.9.1",
-		},
-		Registry: v1alpha1.Registry{
-			Server: "index.docker.io",
-		},
-		Artifact: v1alpha1.Artifact{
-			Repository: "library/alpine",
-			Tag:        "3.10.2",
-		},
-		Summary: v1alpha1.VulnerabilitySummary{
-			CriticalCount: 0,
-			MediumCount:   1,
-			LowCount:      1,
-			NoneCount:     0,
-			UnknownCount:  0,
-		},
-		Vulnerabilities: []v1alpha1.Vulnerability{
-			{
-				VulnerabilityID:  "CVE-2019-1549",
-				Resource:         "openssl",
-				InstalledVersion: "1.1.1c-r0",
-				FixedVersion:     "1.1.1d-r0",
-				Severity:         v1alpha1.SeverityMedium,
-				Title:            "openssl: information disclosure in fork()",
-				PrimaryLink:      "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1549",
-				Links:            []string{},
-			},
-			{
-				VulnerabilityID:  "CVE-2019-1547",
-				Resource:         "openssl",
-				InstalledVersion: "1.1.1c-r0",
-				FixedVersion:     "1.1.1d-r0",
-				Severity:         v1alpha1.SeverityLow,
-				Title:            "openssl: side-channel weak encryption vulnerability",
-				PrimaryLink:      "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1547",
-				Links:            []string{},
-			},
-		},
-	}
-)
-
-func TestConverter_Convert(t *testing.T) {
-	config := starboard.ConfigData{
-		"trivy.imageRef": "aquasec/trivy:0.9.1",
-	}
-
-	testCases := []struct {
-		name           string
-		imageRef       string
-		input          string
-		expectedError  error
-		expectedReport v1alpha1.VulnerabilityScanResult
-	}{
-		{
-			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",
-			imageRef:      "core.harbor.domain/library/nginx@sha256:d20aa6d1cae56fd17cd458f4807e0de462caf2336f0b70b5eeb69fcaaf30dd9c",
-			input:         `null`,
-			expectedError: nil,
-			expectedReport: v1alpha1.VulnerabilityScanResult{
-				Scanner: v1alpha1.Scanner{
-					Name:    "Trivy",
-					Vendor:  "Aqua Security",
-					Version: "0.9.1",
-				},
-				Registry: v1alpha1.Registry{
-					Server: "core.harbor.domain",
-				},
-				Artifact: v1alpha1.Artifact{
-					Repository: "library/nginx",
-					Digest:     "sha256:d20aa6d1cae56fd17cd458f4807e0de462caf2336f0b70b5eeb69fcaaf30dd9c",
-				},
-				Summary: v1alpha1.VulnerabilitySummary{
-					CriticalCount: 0,
-					HighCount:     0,
-					MediumCount:   0,
-					LowCount:      0,
-					NoneCount:     0,
-					UnknownCount:  0,
-				},
-				Vulnerabilities: []v1alpha1.Vulnerability{},
-			},
-		},
-		{
-			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 := trivy.NewConverter(config).Convert(tc.imageRef, strings.NewReader(tc.input))
-			fakeTime := metav1.NewTime(time.Now())
-			report.UpdateTimestamp = fakeTime
-			tc.expectedReport.UpdateTimestamp = fakeTime
-			switch {
-			case tc.expectedError == nil:
-				require.NoError(t, err)
-				assert.Equal(t, tc.expectedReport, report)
-			default:
-				assert.EqualError(t, err, tc.expectedError.Error())
-			}
-		})
-	}
-
-}
-
-func TestGetScoreFromCvss(t *testing.T) {
-	testCases := []struct {
-		name          string
-		cvss          map[string]*trivy.CVSS
-		expectedScore *float64
-	}{
-		{
-			name: "Should return vendor score when vendor v3 score exist",
-			cvss: map[string]*trivy.CVSS{
-				"nvd": {
-					V3Score: pointer.Float64Ptr(8.1),
-				},
-				"redhat": {
-					V3Score: pointer.Float64Ptr(8.3),
-				},
-			},
-			expectedScore: pointer.Float64Ptr(8.3),
-		},
-		{
-			name: "Should return nvd score when vendor v3 score is nil",
-			cvss: map[string]*trivy.CVSS{
-				"nvd": {
-					V3Score: pointer.Float64Ptr(8.1),
-				},
-				"redhat": {
-					V3Score: nil,
-				},
-			},
-			expectedScore: pointer.Float64Ptr(8.1),
-		},
-		{
-			name: "Should return nvd score when vendor doesn't exist",
-			cvss: map[string]*trivy.CVSS{
-				"nvd": {
-					V3Score: pointer.Float64Ptr(8.1),
-				},
-			},
-			expectedScore: pointer.Float64Ptr(8.1),
-		},
-		{
-			name: "Should return nil when vendor and nvd both v3 scores are nil",
-			cvss: map[string]*trivy.CVSS{
-				"nvd": {
-					V3Score: nil,
-				},
-				"redhat": {
-					V3Score: nil,
-				},
-			},
-			expectedScore: nil,
-		},
-		{
-			name:          "Should return nil when cvss doesn't exist",
-			cvss:          map[string]*trivy.CVSS{},
-			expectedScore: nil,
-		},
-	}
-
-	for _, tc := range testCases {
-		t.Run(tc.name, func(t *testing.T) {
-			score := trivy.GetScoreFromCvss(tc.cvss)
-			assert.Equal(t, tc.expectedScore, score)
-		})
-	}
-}
diff --git a/pkg/plugin/trivy/plugin_test.go b/pkg/plugin/trivy/plugin_test.go
index cbeda12..553205d 100644
--- a/pkg/plugin/trivy/plugin_test.go
+++ b/pkg/plugin/trivy/plugin_test.go
@@ -1,8 +1,13 @@
 package trivy_test
 
 import (
+	"errors"
+	"io"
+	"strings"
 	"testing"
+	"time"
 
+	"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
 	"github.com/aquasecurity/starboard/pkg/ext"
 	"github.com/aquasecurity/starboard/pkg/plugin/trivy"
 	"github.com/aquasecurity/starboard/pkg/starboard"
@@ -10,9 +15,15 @@ import (
 	"github.com/stretchr/testify/require"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/utils/pointer"
 )
 
+var (
+	fixedTime  = time.Now()
+	fixedClock = ext.NewFixedClock(fixedTime)
+)
+
 func TestScanner_GetScanJobSpec(t *testing.T) {
 
 	testCases := []struct {
@@ -1346,8 +1357,8 @@ CVE-2019-1543`,
 
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
-			jobSpec, secrets, err := trivy.NewPlugin(ext.NewSimpleIDGenerator(), tc.config).
-				GetScanJobSpec(tc.workloadSpec, nil)
+			instance := trivy.NewPlugin(fixedClock, ext.NewSimpleIDGenerator(), tc.config)
+			jobSpec, secrets, err := instance.GetScanJobSpec(tc.workloadSpec, nil)
 			require.NoError(t, err)
 			assert.Empty(t, secrets)
 			assert.Equal(t, tc.expectedJobSpec, jobSpec)
@@ -1355,3 +1366,221 @@ CVE-2019-1543`,
 	}
 
 }
+
+var (
+	sampleReportAsString = `[{
+		"Target": "alpine:3.10.2 (alpine 3.10.2)",
+		"Type": "alpine",
+		"Vulnerabilities": [
+			{
+				"VulnerabilityID": "CVE-2019-1549",
+				"PkgName": "openssl",
+				"InstalledVersion": "1.1.1c-r0",
+				"FixedVersion": "1.1.1d-r0",
+				"Title": "openssl: information disclosure in fork()",
+				"Description": "Usually this long long description of CVE-2019-1549",
+				"Severity": "MEDIUM",
+				"PrimaryURL": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1549",
+				"References": [
+					"https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1549"
+				]
+			},
+			{
+				"VulnerabilityID": "CVE-2019-1547",
+				"PkgName": "openssl",
+				"InstalledVersion": "1.1.1c-r0",
+				"FixedVersion": "1.1.1d-r0",
+				"Title": "openssl: side-channel weak encryption vulnerability",
+				"Severity": "LOW",
+				"PrimaryURL": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1547",
+				"References": [
+					"https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1547"
+				]
+			}
+		]
+	}]`
+
+	sampleReport = v1alpha1.VulnerabilityScanResult{
+		UpdateTimestamp: metav1.NewTime(fixedTime),
+		Scanner: v1alpha1.Scanner{
+			Name:    "Trivy",
+			Vendor:  "Aqua Security",
+			Version: "0.9.1",
+		},
+		Registry: v1alpha1.Registry{
+			Server: "index.docker.io",
+		},
+		Artifact: v1alpha1.Artifact{
+			Repository: "library/alpine",
+			Tag:        "3.10.2",
+		},
+		Summary: v1alpha1.VulnerabilitySummary{
+			CriticalCount: 0,
+			MediumCount:   1,
+			LowCount:      1,
+			NoneCount:     0,
+			UnknownCount:  0,
+		},
+		Vulnerabilities: []v1alpha1.Vulnerability{
+			{
+				VulnerabilityID:  "CVE-2019-1549",
+				Resource:         "openssl",
+				InstalledVersion: "1.1.1c-r0",
+				FixedVersion:     "1.1.1d-r0",
+				Severity:         v1alpha1.SeverityMedium,
+				Title:            "openssl: information disclosure in fork()",
+				PrimaryLink:      "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1549",
+				Links:            []string{},
+			},
+			{
+				VulnerabilityID:  "CVE-2019-1547",
+				Resource:         "openssl",
+				InstalledVersion: "1.1.1c-r0",
+				FixedVersion:     "1.1.1d-r0",
+				Severity:         v1alpha1.SeverityLow,
+				Title:            "openssl: side-channel weak encryption vulnerability",
+				PrimaryLink:      "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1547",
+				Links:            []string{},
+			},
+		},
+	}
+)
+
+func TestScanner_ParseVulnerabilityScanResult(t *testing.T) {
+	config := starboard.ConfigData{
+		"trivy.imageRef": "aquasec/trivy:0.9.1",
+	}
+
+	testCases := []struct {
+		name           string
+		imageRef       string
+		input          string
+		expectedError  error
+		expectedReport v1alpha1.VulnerabilityScanResult
+	}{
+		{
+			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",
+			imageRef:      "core.harbor.domain/library/nginx@sha256:d20aa6d1cae56fd17cd458f4807e0de462caf2336f0b70b5eeb69fcaaf30dd9c",
+			input:         `null`,
+			expectedError: nil,
+			expectedReport: v1alpha1.VulnerabilityScanResult{
+				UpdateTimestamp: metav1.NewTime(fixedTime),
+				Scanner: v1alpha1.Scanner{
+					Name:    "Trivy",
+					Vendor:  "Aqua Security",
+					Version: "0.9.1",
+				},
+				Registry: v1alpha1.Registry{
+					Server: "core.harbor.domain",
+				},
+				Artifact: v1alpha1.Artifact{
+					Repository: "library/nginx",
+					Digest:     "sha256:d20aa6d1cae56fd17cd458f4807e0de462caf2336f0b70b5eeb69fcaaf30dd9c",
+				},
+				Summary: v1alpha1.VulnerabilitySummary{
+					CriticalCount: 0,
+					HighCount:     0,
+					MediumCount:   0,
+					LowCount:      0,
+					NoneCount:     0,
+					UnknownCount:  0,
+				},
+				Vulnerabilities: []v1alpha1.Vulnerability{},
+			},
+		},
+		{
+			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) {
+			instance := trivy.NewPlugin(fixedClock, ext.NewSimpleIDGenerator(), config)
+			report, err := instance.ParseVulnerabilityScanResult(tc.imageRef, io.NopCloser(strings.NewReader(tc.input)))
+			switch {
+			case tc.expectedError == nil:
+				require.NoError(t, err)
+				assert.Equal(t, tc.expectedReport, report)
+			default:
+				assert.EqualError(t, err, tc.expectedError.Error())
+			}
+		})
+	}
+
+}
+
+func TestGetScoreFromCVSS(t *testing.T) {
+	testCases := []struct {
+		name          string
+		cvss          map[string]*trivy.CVSS
+		expectedScore *float64
+	}{
+		{
+			name: "Should return vendor score when vendor v3 score exist",
+			cvss: map[string]*trivy.CVSS{
+				"nvd": {
+					V3Score: pointer.Float64Ptr(8.1),
+				},
+				"redhat": {
+					V3Score: pointer.Float64Ptr(8.3),
+				},
+			},
+			expectedScore: pointer.Float64Ptr(8.3),
+		},
+		{
+			name: "Should return nvd score when vendor v3 score is nil",
+			cvss: map[string]*trivy.CVSS{
+				"nvd": {
+					V3Score: pointer.Float64Ptr(8.1),
+				},
+				"redhat": {
+					V3Score: nil,
+				},
+			},
+			expectedScore: pointer.Float64Ptr(8.1),
+		},
+		{
+			name: "Should return nvd score when vendor doesn't exist",
+			cvss: map[string]*trivy.CVSS{
+				"nvd": {
+					V3Score: pointer.Float64Ptr(8.1),
+				},
+			},
+			expectedScore: pointer.Float64Ptr(8.1),
+		},
+		{
+			name: "Should return nil when vendor and nvd both v3 scores are nil",
+			cvss: map[string]*trivy.CVSS{
+				"nvd": {
+					V3Score: nil,
+				},
+				"redhat": {
+					V3Score: nil,
+				},
+			},
+			expectedScore: nil,
+		},
+		{
+			name:          "Should return nil when cvss doesn't exist",
+			cvss:          map[string]*trivy.CVSS{},
+			expectedScore: nil,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			score := trivy.GetScoreFromCVSS(tc.cvss)
+			assert.Equal(t, tc.expectedScore, score)
+		})
+	}
+}
diff --git a/pkg/vulnerabilityreport/scanner_test.go b/pkg/vulnerabilityreport/scanner_test.go
new file mode 100644
index 0000000..d36e439
--- /dev/null
+++ b/pkg/vulnerabilityreport/scanner_test.go
@@ -0,0 +1,1 @@
+package vulnerabilityreport_test