From e33cd12bb7379283b493a28d86aea51496ca1bfe Mon Sep 17 00:00:00 2001 From: Karan Kumar Date: Wed, 7 Feb 2024 22:26:50 +0530 Subject: [PATCH] Implement vulnerability scanning for container images - implement vulnerability scanning using trivy - list vulnerabilities in buildrun output --- cmd/image-processing/main.go | 39 ++++++ cmd/image-processing/main_test.go | 85 +++++++++++++ pkg/image/vulnerability_scan.go | 118 ++++++++++++++++++ pkg/image/vulnerability_scan_test.go | 103 +++++++++++++++ pkg/reconciler/buildrun/buildrun.go | 11 ++ pkg/reconciler/buildrun/buildrun_test.go | 82 ++++++++++++ .../buildrun/resources/image_processing.go | 6 + .../resources/image_processing_test.go | 8 ++ pkg/reconciler/buildrun/resources/results.go | 15 ++- .../buildrun/resources/results_test.go | 10 +- .../buildrun/resources/taskrun_test.go | 4 + test/integration/build_to_taskruns_test.go | 2 + .../buildstrategy_to_taskruns_test.go | 3 +- test/v1alpha1_samples/catalog.go | 26 ++++ 14 files changed, 508 insertions(+), 4 deletions(-) create mode 100644 pkg/image/vulnerability_scan.go create mode 100644 pkg/image/vulnerability_scan_test.go diff --git a/cmd/image-processing/main.go b/cmd/image-processing/main.go index 2f93db31bb..c705110e4c 100644 --- a/cmd/image-processing/main.go +++ b/cmd/image-processing/main.go @@ -9,6 +9,7 @@ package main import ( "context" + "encoding/json" "fmt" "log" "os" @@ -17,6 +18,7 @@ import ( "github.com/google/go-containerregistry/pkg/name" containerreg "github.com/google/go-containerregistry/pkg/v1" + shipwrightv1alpha1 "github.com/shipwright-io/build/pkg/apis/build/v1alpha1" "github.com/shipwright-io/build/pkg/image" "github.com/spf13/pflag" ) @@ -42,7 +44,9 @@ type settings struct { image, resultFileImageDigest, resultFileImageSize, + resultFileImageVulnerabilities, secretPath string + vulnerabilitySettings shipwrightv1alpha1.VulnerabilityScanOptions } func getAnnotation() []string { @@ -83,6 +87,9 @@ func initializeFlag() { flagValues.label = pflag.StringArray("label", nil, "New labels to add") pflag.StringVar(&flagValues.resultFileImageDigest, "result-file-image-digest", "", "A file to write the image digest to") pflag.StringVar(&flagValues.resultFileImageSize, "result-file-image-size", "", "A file to write the image size to") + pflag.StringVar(&flagValues.resultFileImageVulnerabilities, "result-file-image-vulnerabilities", "", "A file to write the image vulnerabilities to") + + pflag.Var(&flagValues.vulnerabilitySettings, "vuln-settings", "Vulnerability Settings") } func main() { @@ -171,6 +178,38 @@ func runImageProcessing(ctx context.Context) error { } } + // check for image vulnerabilities if vulnerability scanning is enabled. + var vulns []shipwrightv1alpha1.Vulnerability + if flagValues.vulnerabilitySettings.Enabled && flagValues.push != "" { + imagePath := flagValues.push + if img != nil { + entries, err := os.ReadDir(flagValues.push) + if err != nil { + return err + } + imagePath = imagePath + "/" + entries[0].Name() + } + vulns, err = image.RunVulnerabilityScan(ctx, imagePath, flagValues.vulnerabilitySettings) + if err != nil { + return err + } + vulnsData, err := json.Marshal(vulns) + if err != nil { + return err + } + if err := os.WriteFile(flagValues.resultFileImageVulnerabilities, vulnsData, 0640); err != nil { + return err + } + } + + if flagValues.vulnerabilitySettings.FailPush && len(vulns) > 0 { + errMsg := "vulnerabilities have been found in the output image." + log.Println(errMsg) + + // if failPush is set then no need to push the image + return nil + } + // push the image and determine the digest and size log.Printf("Pushing the image to registry %q\n", imageName.String()) digest, size, err := image.PushImageOrImageIndex(imageName, img, imageIndex, options) diff --git a/cmd/image-processing/main_test.go b/cmd/image-processing/main_test.go index 30d7a38b16..d0843c477a 100644 --- a/cmd/image-processing/main_test.go +++ b/cmd/image-processing/main_test.go @@ -11,7 +11,9 @@ import ( "net/http/httptest" "net/url" "os" + "path" "strconv" + "strings" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -22,6 +24,7 @@ import ( containerreg "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/remote" + shipwrightv1alpha1 "github.com/shipwright-io/build/pkg/apis/build/v1alpha1" "github.com/spf13/pflag" "k8s.io/apimachinery/pkg/util/rand" ) @@ -300,4 +303,86 @@ var _ = Describe("Image Processing Resource", func() { }) }) }) + + Context("vulnerability scanning", func() { + cwd, err := os.Getwd() + Expect(err).ToNot(HaveOccurred()) + directory := path.Clean(path.Join(cwd, "../..", "test/data/images/vuln-image-in-oci")) + It("should run vulnerability scanning if it is enabled", func() { + vulnOptions := shipwrightv1alpha1.VulnerabilityScanOptions{ + Enabled: true, + } + withTempRegistry(func(endpoint string) { + tag, err := name.NewTag(fmt.Sprintf("%s/%s:%s", endpoint, "temp-image", rand.String(5))) + Expect(err).ToNot(HaveOccurred()) + withTempFile("vuln-scan-result", func(filename string) { + Expect(run( + "--insecure", + "--image", tag.String(), + "--push", directory, + "--vuln-settings", vulnOptions.String(), + "--result-file-image-vulnerabilities", filename, + )).ToNot(HaveOccurred()) + Expect(strings.Contains(filecontent(filename), "CVE-2019-8457")).To(BeTrue()) + }) + }) + }) + + It("should push the image if vulnerabilities are found and failPush is false", func() { + vulnOptions := shipwrightv1alpha1.VulnerabilityScanOptions{ + Enabled: true, + FailPush: false, + } + + withTempRegistry(func(endpoint string) { + tag, err := name.NewTag(fmt.Sprintf("%s/%s:%s", endpoint, "temp-image", rand.String(5))) + Expect(err).ToNot(HaveOccurred()) + withTempFile("vuln-scan-result", func(filename string) { + Expect(run( + "--insecure", + "--image", tag.String(), + "--push", directory, + "--vuln-settings", vulnOptions.String(), + "--result-file-image-vulnerabilities", filename, + )).ToNot(HaveOccurred()) + Expect(strings.Contains(filecontent(filename), "CVE-2019-8457")).To(BeTrue()) + }) + + ref, err := name.ParseReference(tag.String()) + Expect(err).ToNot(HaveOccurred()) + + _, err = remote.Get(ref) + Expect(err).ToNot(HaveOccurred()) + + }) + }) + + It("should not push the image if vulnerabilities are found and failPush is true", func() { + vulnOptions := shipwrightv1alpha1.VulnerabilityScanOptions{ + Enabled: true, + FailPush: true, + } + withTempRegistry(func(endpoint string) { + tag, err := name.NewTag(fmt.Sprintf("%s/%s:%s", endpoint, "temp-image", rand.String(5))) + Expect(err).ToNot(HaveOccurred()) + withTempFile("vuln-scan-result", func(filename string) { + Expect(run( + "--insecure", + "--image", tag.String(), + "--push", directory, + "--vuln-settings", vulnOptions.String(), + "--result-file-image-vulnerabilities", filename, + )).ToNot(HaveOccurred()) + Expect(strings.Contains(filecontent(filename), "CVE-2019-8457")).To(BeTrue()) + }) + + ref, err := name.ParseReference(tag.String()) + Expect(err).ToNot(HaveOccurred()) + + _, err = remote.Get(ref) + Expect(err).To(HaveOccurred()) + + }) + }) + }) }) diff --git a/pkg/image/vulnerability_scan.go b/pkg/image/vulnerability_scan.go new file mode 100644 index 0000000000..6cbe069d7c --- /dev/null +++ b/pkg/image/vulnerability_scan.go @@ -0,0 +1,118 @@ +// Copyright The Shipwright Contributors +// +// SPDX-License-Identifier: Apache-2.0 +package image + +import ( + "context" + "encoding/json" + "fmt" + "log" + "os" + "os/exec" + "sort" + "strconv" + "strings" + + shipwrightv1alpha1 "github.com/shipwright-io/build/pkg/apis/build/v1alpha1" +) + +const ( + VulnerabilityCountLimit = 50 // Number of vulnerabilities to be added to buildrun output. +) + +func RunVulnerabilityScan(ctx context.Context, imagePath string, settings shipwrightv1alpha1.VulnerabilityScanOptions) ([]shipwrightv1alpha1.Vulnerability, error) { + + trivyArgs := []string{"image", "--quiet", "--format", "json", "--input", imagePath} + + if settings.IgnoreOptions != nil { + if settings.IgnoreOptions.Severity != "" { + severity := getSeverity(settings.IgnoreOptions.Severity) + trivyArgs = append(trivyArgs, "--severity", severity) + } + if len(settings.IgnoreOptions.Issues) > 0 { + // Create a file with vulnerabilities to be ignored. + ignoreFile, err := os.CreateTemp("", "ignore") + if err != nil { + log.Printf("Could not create file for ignored vulnerabilities: %v\n", err) + return nil, err + } + defer os.Remove(ignoreFile.Name()) + for _, vul := range settings.IgnoreOptions.Issues { + _, err := ignoreFile.WriteString(vul + "\n") + if err != nil { + log.Printf("Error writing to ignore file for vulnerabilities: %v\n", err) + return nil, err + } + } + ignoreFile.Close() + trivyArgs = append(trivyArgs, "--ignorefile", ignoreFile.Name()) + } + trivyArgs = append(trivyArgs, "--ignore-unfixed", strconv.FormatBool(settings.IgnoreOptions.Unfixed)) + } + + cmd := exec.CommandContext(ctx, "trivy", trivyArgs...) + + // Print the command to be executed + log.Println(cmd.String()) + + cmd.Stdin = nil + + result, err := cmd.CombinedOutput() + + if err != nil { + log.Println("failed to run trivy : ", string(result)) + return nil, fmt.Errorf("failed to run trivy: %w", err) + } + + type TrivyResult struct { + Results []struct { + Vulnerabilities []shipwrightv1alpha1.Vulnerability `json:"Vulnerabilities"` + } `json:"Results"` + } + + var trivyResult TrivyResult + if err := json.Unmarshal(result, &trivyResult); err != nil { + return nil, err + } + + var vulns []shipwrightv1alpha1.Vulnerability + for _, result := range trivyResult.Results { + vulns = append(vulns, result.Vulnerabilities...) + } + + //Sort the vulnerabilities by severity + sort.Slice(vulns, func(i, j int) bool { + severityOrder := map[string]int{"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3, "UNKNOWN": 4} + return severityOrder[vulns[i].Severity] < severityOrder[vulns[j].Severity] + }) + + if len(vulns) > VulnerabilityCountLimit { + vulns = vulns[:VulnerabilityCountLimit] + } + + return vulns, nil +} + +func getSeverity(ignoreSeverities string) string { + excludeSeverities := strings.Split(ignoreSeverities, ",") + + // Convert all strings to uppercase + for i, str := range excludeSeverities { + excludeSeverities[i] = strings.ToUpper(str) + } + + supportedSeverities := []string{"UNKNOWN", "LOW", "MEDIUM", "HIGH", "CRITICAL"} + excludeSet := make(map[string]bool, len(excludeSeverities)) + for _, v := range excludeSeverities { + excludeSet[v] = true + } + + var selected []string + for _, value := range supportedSeverities { + if !excludeSet[value] { + selected = append(selected, value) + } + } + return strings.Join(selected, ",") +} diff --git a/pkg/image/vulnerability_scan_test.go b/pkg/image/vulnerability_scan_test.go new file mode 100644 index 0000000000..008b21b3dd --- /dev/null +++ b/pkg/image/vulnerability_scan_test.go @@ -0,0 +1,103 @@ +// Copyright The Shipwright Contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package image_test + +import ( + "context" + "os" + "path" + + shipwrightv1alpha1 "github.com/shipwright-io/build/pkg/apis/build/v1alpha1" + "github.com/shipwright-io/build/pkg/image" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("VulnerabilityScanning", func() { + + Context("For an index", func() { + vulnOptions := shipwrightv1alpha1.VulnerabilityScanOptions{ + Enabled: true, + } + cwd, err := os.Getwd() + Expect(err).ToNot(HaveOccurred()) + directory := path.Clean(path.Join(cwd, "../..", "test/data/images/vuln-image-in-oci")) + + It("runs the image vulnerability scan", func() { + //Run vulnerability Scan + vulns, err := image.RunVulnerabilityScan(context.TODO(), directory, vulnOptions) + Expect(err).ToNot(HaveOccurred()) + Expect(vulns).NotTo(BeNil()) + Expect(len(vulns)).Should(BeNumerically(">", 0)) + }) + }) + + Context("For an image", func() { + cwd, err := os.Getwd() + Expect(err).ToNot(HaveOccurred()) + directory := path.Clean(path.Join(cwd, "../..", "test/data/images/vuln-single-image/vuln-image.tar")) + + It("runs the image vulnerability scan", func() { + //Run vulnerability Scan + vulns, err := image.RunVulnerabilityScan(context.TODO(), directory, shipwrightv1alpha1.VulnerabilityScanOptions{}) + Expect(err).ToNot(HaveOccurred()) + Expect(vulns).NotTo(BeNil()) + Expect(len(vulns)).Should(BeNumerically(">", 0)) + }) + + It("should ignore the severity defined in ignore options", func() { + vulnOptions := shipwrightv1alpha1.VulnerabilityScanOptions{ + Enabled: true, + IgnoreOptions: &shipwrightv1alpha1.VulnerabilityIgnoreOptions{ + Severity: "LOW", + }, + } + // Run vulnerability Scan + vulns, err := image.RunVulnerabilityScan(context.TODO(), directory, vulnOptions) + Expect(err).ToNot(HaveOccurred()) + Expect(vulns).NotTo(BeNil()) + Expect(len(vulns)).Should(BeNumerically(">", 0)) + Expect(severityExists(vulns, "LOW")).To(BeFalse()) + }) + + It("should ignore the vulnerabilities defined in ignore options", func() { + vulnOptions := shipwrightv1alpha1.VulnerabilityScanOptions{ + Enabled: true, + IgnoreOptions: &shipwrightv1alpha1.VulnerabilityIgnoreOptions{ + Issues: []string{ + "CVE-2018-20843", + }, + Unfixed: true, + }, + } + // Run Vulnerability Scan + vulns, err := image.RunVulnerabilityScan(context.TODO(), directory, vulnOptions) + Expect(err).ToNot(HaveOccurred()) + Expect(vulns).NotTo(BeNil()) + Expect(len(vulns)).Should(BeNumerically(">", 0)) + Expect(vulnerabilityExists(vulns, vulnOptions.IgnoreOptions.Issues[0])).To(BeFalse()) + }) + + }) +}) + +func severityExists(vulns []shipwrightv1alpha1.Vulnerability, severity string) bool { + for _, vuln := range vulns { + if vuln.Severity == severity { + return true + } + } + return false +} + +func vulnerabilityExists(vulns []shipwrightv1alpha1.Vulnerability, vulnerability string) bool { + for _, vuln := range vulns { + if vuln.VulnerabilityID == vulnerability { + return true + } + } + return false +} diff --git a/pkg/reconciler/buildrun/buildrun.go b/pkg/reconciler/buildrun/buildrun.go index eb7ab6ba7f..2576a6d686 100644 --- a/pkg/reconciler/buildrun/buildrun.go +++ b/pkg/reconciler/buildrun/buildrun.go @@ -466,6 +466,17 @@ func (r *ReconcileBuildRun) Reconcile(ctx context.Context, request reconcile.Req } } + if buildRun.Status.Output != nil { + if len(buildRun.Status.Output.Vulnerabilities) > 0 { + if buildRun.Spec.BuildSpec.Output.VulnerabilityScan.FailPush { + return reconcile.Result{}, resources.UpdateConditionWithFalseStatus(ctx, r.client, buildRun, fmt.Sprintf("Vulnerabilities have been found in the output image. For detailed information, see kubectl --namespace %s logs %s --container step-image-processing", + buildRun.Namespace, + lastTaskRun.Status.PodName, + ), "VulnerabilitiesFound") + } + } + } + ctxlog.Info(ctx, "updating buildRun status", namespace, request.Namespace, name, request.Name) if err := r.client.Status().Update(ctx, buildRun); err != nil { return reconcile.Result{}, err diff --git a/pkg/reconciler/buildrun/buildrun_test.go b/pkg/reconciler/buildrun/buildrun_test.go index 1f95f07649..0219926720 100644 --- a/pkg/reconciler/buildrun/buildrun_test.go +++ b/pkg/reconciler/buildrun/buildrun_test.go @@ -342,6 +342,88 @@ var _ = Describe("Reconcile BuildRun", func() { Expect(client.StatusCallCount()).To(Equal(1)) }) + It("updates the BuildRun status with a VulnerabilitiesFound reason", func() { + buildRunSample = ctl.DefaultBuildRunWithoutBuild(buildRunName) + taskRunSample = ctl.DefaultTaskRunWithStatus(taskRunName, buildRunName, ns, corev1.ConditionTrue, "Succeeded") + + buildRunSample.Spec.BuildSpec.Output = build.Image{ + VulnerabilityScan: &build.VulnerabilityScanOptions{ + FailPush: true, + }, + } + vulns := `[{"vulnerabilityID":"CVE-2019-12900","severity":"CRITICAL"},{"vulnerabilityID":"CVE-2019-8457","severity":"CRITICAL"}]` + taskRunSample.Status.TaskRunStatusFields.Results = append(taskRunSample.Status.Results, pipelineapi.TaskRunResult{ + Name: "shp-image-vulnerabilities", + Value: pipelineapi.ParamValue{ + Type: pipelineapi.ParamTypeString, + StringVal: vulns, + }, + }) + + // Stub that asserts the BuildRun status fields when + // Status updates for a BuildRun take place + statusCall := ctl.StubBuildRunStatus( + "Succeeded", + &taskRunName, + build.Condition{ + Type: build.Succeeded, + Reason: "VulnerabilitiesFound", + Status: corev1.ConditionFalse, + }, + corev1.ConditionTrue, + buildSample.Spec, + false, + ) + statusWriter.UpdateCalls(statusCall) + + result, err := reconciler.Reconcile(context.TODO(), taskRunRequest) + Expect(err).ToNot(HaveOccurred()) + Expect(reconcile.Result{}).To(Equal(result)) + Expect(client.GetCallCount()).To(Equal(2)) + Expect(client.StatusCallCount()).To(Equal(1)) + }) + + It("updates the BuildRun status with a SUCCEEDED reason if FailPush in vulnerabilities options is false", func() { + buildRunSample = ctl.DefaultBuildRunWithoutBuild(buildRunName) + taskRunSample = ctl.DefaultTaskRunWithStatus(taskRunName, buildRunName, ns, corev1.ConditionTrue, "Succeeded") + + buildRunSample.Spec.BuildSpec.Output = build.Image{ + VulnerabilityScan: &build.VulnerabilityScanOptions{ + FailPush: false, + }, + } + vulns := `[{"vulnerabilityID":"CVE-2019-12900","severity":"CRITICAL"},{"vulnerabilityID":"CVE-2019-8457","severity":"CRITICAL"}]` + taskRunSample.Status.TaskRunStatusFields.Results = append(taskRunSample.Status.Results, pipelineapi.TaskRunResult{ + Name: "shp-image-vulnerabilities", + Value: pipelineapi.ParamValue{ + Type: pipelineapi.ParamTypeString, + StringVal: vulns, + }, + }) + + // Stub that asserts the BuildRun status fields when + // Status updates for a BuildRun take place + statusCall := ctl.StubBuildRunStatus( + "Succeeded", + &taskRunName, + build.Condition{ + Type: build.Succeeded, + Reason: "Succeeded", + Status: corev1.ConditionTrue, + }, + corev1.ConditionTrue, + buildSample.Spec, + false, + ) + statusWriter.UpdateCalls(statusCall) + + result, err := reconciler.Reconcile(context.TODO(), taskRunRequest) + Expect(err).ToNot(HaveOccurred()) + Expect(reconcile.Result{}).To(Equal(result)) + Expect(client.GetCallCount()).To(Equal(2)) + Expect(client.StatusCallCount()).To(Equal(1)) + }) + It("should recognize the BuildRun is canceled", func() { // set cancel buildRunSampleCopy := buildRunSample.DeepCopy() diff --git a/pkg/reconciler/buildrun/resources/image_processing.go b/pkg/reconciler/buildrun/resources/image_processing.go index 349ed6af63..c347153034 100644 --- a/pkg/reconciler/buildrun/resources/image_processing.go +++ b/pkg/reconciler/buildrun/resources/image_processing.go @@ -93,6 +93,12 @@ func SetupImageProcessing(taskRun *pipelineapi.TaskRun, cfg *config.Config, buil // add the result arguments stepArgs = append(stepArgs, "--result-file-image-digest", fmt.Sprintf("$(results.%s-%s.path)", prefixParamsResultsVolumes, imageDigestResult)) stepArgs = append(stepArgs, "--result-file-image-size", fmt.Sprintf("$(results.%s-%s.path)", prefixParamsResultsVolumes, imageSizeResult)) + stepArgs = append(stepArgs, "--result-file-image-vulnerabilities", fmt.Sprintf("$(results.%s-%s.path)", prefixParamsResultsVolumes, imageVulnerabilities)) + + // add vulnerability scan arguments + if buildOutput.VulnerabilityScan != nil { + stepArgs = append(stepArgs, "--vuln-settings", buildOutput.VulnerabilityScan.String()) + } // add the push step diff --git a/pkg/reconciler/buildrun/resources/image_processing_test.go b/pkg/reconciler/buildrun/resources/image_processing_test.go index 0e7853a854..0b860ac738 100644 --- a/pkg/reconciler/buildrun/resources/image_processing_test.go +++ b/pkg/reconciler/buildrun/resources/image_processing_test.go @@ -75,6 +75,8 @@ var _ = Describe("Image Processing overrides", func() { "$(results.shp-image-digest.path)", "--result-file-image-size", "$(results.shp-image-size.path)", + "--result-file-image-vulnerabilities", + "$(results.shp-image-vulnerabilities.path)", })) Expect(processedTaskRun.Spec.TaskSpec.Steps[1].VolumeMounts).ToNot(utils.ContainNamedElement("shp-output-directory")) }) @@ -143,6 +145,8 @@ var _ = Describe("Image Processing overrides", func() { "$(results.shp-image-digest.path)", "--result-file-image-size", "$(results.shp-image-size.path)", + "--result-file-image-vulnerabilities", + "$(results.shp-image-vulnerabilities.path)", })) Expect(processedTaskRun.Spec.TaskSpec.Steps[1].VolumeMounts).To(utils.ContainNamedElement("shp-output-directory")) }) @@ -180,6 +184,8 @@ var _ = Describe("Image Processing overrides", func() { "$(results.shp-image-digest.path)", "--result-file-image-size", "$(results.shp-image-size.path)", + "--result-file-image-vulnerabilities", + "$(results.shp-image-vulnerabilities.path)", })) Expect(processedTaskRun.Spec.TaskSpec.Steps[1].VolumeMounts).To(utils.ContainNamedElement("shp-output-directory")) }) @@ -225,6 +231,8 @@ var _ = Describe("Image Processing overrides", func() { "$(results.shp-image-digest.path)", "--result-file-image-size", "$(results.shp-image-size.path)", + "--result-file-image-vulnerabilities", + "$(results.shp-image-vulnerabilities.path)", "--secret-path", "/workspace/shp-push-secret", })) diff --git a/pkg/reconciler/buildrun/resources/results.go b/pkg/reconciler/buildrun/resources/results.go index 61b31a9d3c..aa7ebbc2ce 100644 --- a/pkg/reconciler/buildrun/resources/results.go +++ b/pkg/reconciler/buildrun/resources/results.go @@ -6,6 +6,7 @@ package resources import ( "context" + "encoding/json" "fmt" "strconv" @@ -17,8 +18,9 @@ import ( ) const ( - imageDigestResult = "image-digest" - imageSizeResult = "image-size" + imageDigestResult = "image-digest" + imageSizeResult = "image-size" + imageVulnerabilities = "image-vulnerabilities" ) // UpdateBuildRunUsingTaskResults surface the task results @@ -51,6 +53,11 @@ func updateBuildRunStatusWithOutputResult(ctx context.Context, buildRun *build.B } else { buildRun.Status.Output.Size = size } + case generateOutputResultName(imageVulnerabilities): + if err := json.Unmarshal([]byte(result.Value.StringVal), &buildRun.Status.Output.Vulnerabilities); err != nil { + ctxlog.Info(ctx, "failed to unmarshal vulnerabilities list", namespace, request.Namespace, name, request.Name, "error", err) + buildRun.Status.Output.Vulnerabilities = nil + } } } } @@ -69,5 +76,9 @@ func getTaskSpecResults() []pipelineapi.TaskResult { Name: fmt.Sprintf("%s-%s", prefixParamsResultsVolumes, imageSizeResult), Description: "The compressed size of the image", }, + { + Name: fmt.Sprintf("%s-%s", prefixParamsResultsVolumes, imageVulnerabilities), + Description: "List of vulnerabilities", + }, } } diff --git a/pkg/reconciler/buildrun/resources/results_test.go b/pkg/reconciler/buildrun/resources/results_test.go index 5196d8da4b..c985575231 100644 --- a/pkg/reconciler/buildrun/resources/results_test.go +++ b/pkg/reconciler/buildrun/resources/results_test.go @@ -98,7 +98,7 @@ var _ = Describe("TaskRun results to BuildRun", func() { It("should surface the TaskRun results emitting from output step", func() { imageDigest := "sha256:fe1b73cd25ac3f11dec752755e2" - + vulns := `[{"vulnerabilityID":"CVE-2019-12900","severity":"CRITICAL"},{"vulnerabilityID":"CVE-2019-8457","severity":"CRITICAL"}]` tr.Status.Results = append(tr.Status.Results, pipelineapi.TaskRunResult{ Name: "shp-image-digest", @@ -113,12 +113,20 @@ var _ = Describe("TaskRun results to BuildRun", func() { Type: pipelineapi.ParamTypeString, StringVal: "230", }, + }, + pipelineapi.TaskRunResult{ + Name: "shp-image-vulnerabilities", + Value: pipelineapi.ParamValue{ + Type: pipelineapi.ParamTypeString, + StringVal: vulns, + }, }) resources.UpdateBuildRunUsingTaskResults(ctx, br, tr.Status.Results, taskRunRequest) Expect(br.Status.Output.Digest).To(Equal(imageDigest)) Expect(br.Status.Output.Size).To(Equal(int64(230))) + Expect(len(br.Status.Output.Vulnerabilities)).To(Equal(2)) }) It("should surface the TaskRun results emitting from source and output step", func() { diff --git a/pkg/reconciler/buildrun/resources/taskrun_test.go b/pkg/reconciler/buildrun/resources/taskrun_test.go index 3a4f21a5fd..6b1e069091 100644 --- a/pkg/reconciler/buildrun/resources/taskrun_test.go +++ b/pkg/reconciler/buildrun/resources/taskrun_test.go @@ -168,6 +168,8 @@ var _ = Describe("GenerateTaskrun", func() { "$(results.shp-image-digest.path)", "--result-file-image-size", "$(results.shp-image-size.path)", + "--result-file-image-vulnerabilities", + "$(results.shp-image-vulnerabilities.path)", })) }) @@ -198,6 +200,8 @@ var _ = Describe("GenerateTaskrun", func() { "$(results.shp-image-digest.path)", "--result-file-image-size", "$(results.shp-image-size.path)", + "--result-file-image-vulnerabilities", + "$(results.shp-image-vulnerabilities.path)", } Expect(got.Steps[3].Args).To(HaveLen(len(expected))) diff --git a/test/integration/build_to_taskruns_test.go b/test/integration/build_to_taskruns_test.go index 6766cfcd3b..54d841a2fd 100644 --- a/test/integration/build_to_taskruns_test.go +++ b/test/integration/build_to_taskruns_test.go @@ -175,6 +175,8 @@ var _ = Describe("Integration tests Build and TaskRun", func() { "$(results.shp-image-digest.path)", "--result-file-image-size", "$(results.shp-image-size.path)", + "--result-file-image-vulnerabilities", + "$(results.shp-image-vulnerabilities.path)", })) }) diff --git a/test/integration/buildstrategy_to_taskruns_test.go b/test/integration/buildstrategy_to_taskruns_test.go index adc6064812..e2411ec778 100644 --- a/test/integration/buildstrategy_to_taskruns_test.go +++ b/test/integration/buildstrategy_to_taskruns_test.go @@ -587,7 +587,6 @@ var _ = Describe("Integration tests BuildStrategies and TaskRuns", func() { "--result-file-image-digest", "$(results.shp-image-digest.path)", "--result-file-image-size", - "$(results.shp-image-size.path)", })) }) }) @@ -646,6 +645,8 @@ var _ = Describe("Integration tests BuildStrategies and TaskRuns", func() { "$(results.shp-image-digest.path)", "--result-file-image-size", "$(results.shp-image-size.path)", + "--result-file-image-vulnerabilities", + "$(results.shp-image-vulnerabilities.path)", "--secret-path", "/workspace/shp-push-secret", })) diff --git a/test/v1alpha1_samples/catalog.go b/test/v1alpha1_samples/catalog.go index 15a64b0ed5..a297974c7d 100644 --- a/test/v1alpha1_samples/catalog.go +++ b/test/v1alpha1_samples/catalog.go @@ -783,6 +783,32 @@ func (c *Catalog) DefaultBuildRun(buildRunName string, buildName string) *build. } } +// DefaultBuildRunWithoutBuild returns a minimal BuildRun object without Build reference +func (c *Catalog) DefaultBuildRunWithoutBuild(buildRunName string) *build.BuildRun { + var strategy build.BuildStrategyKind = "ClusterBuildStrategy" + return &build.BuildRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: buildRunName, + }, + Spec: build.BuildRunSpec{ + BuildSpec: &build.BuildSpec{ + Strategy: build.Strategy{ + Name: "foobar-strategy", + Kind: &strategy, + }, + }, + }, + Status: build.BuildRunStatus{ + BuildSpec: &build.BuildSpec{ + Strategy: build.Strategy{ + Name: "foobar-strategy", + Kind: &strategy, + }, + }, + }, + } +} + // PodWithInitContainerStatus returns a pod with a single // entry under the Status field for InitContainer Status func (c *Catalog) PodWithInitContainerStatus(podName string, initContainerName string) *corev1.Pod {