Skip to content

Commit

Permalink
Implement vulnerability scanning for container images
Browse files Browse the repository at this point in the history
- implement vulnerability scanning using trivy
- list vulnerabilities in buildrun output
  • Loading branch information
karanibm6 committed Feb 7, 2024
1 parent 0191bfd commit e33cd12
Show file tree
Hide file tree
Showing 14 changed files with 508 additions and 4 deletions.
39 changes: 39 additions & 0 deletions cmd/image-processing/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package main

import (
"context"
"encoding/json"
"fmt"
"log"
"os"
Expand All @@ -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"
)
Expand All @@ -42,7 +44,9 @@ type settings struct {
image,
resultFileImageDigest,
resultFileImageSize,
resultFileImageVulnerabilities,
secretPath string
vulnerabilitySettings shipwrightv1alpha1.VulnerabilityScanOptions
}

func getAnnotation() []string {
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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)
Expand Down
85 changes: 85 additions & 0 deletions cmd/image-processing/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import (
"net/http/httptest"
"net/url"
"os"
"path"
"strconv"
"strings"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
Expand All @@ -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"
)
Expand Down Expand Up @@ -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())

})
})
})
})
118 changes: 118 additions & 0 deletions pkg/image/vulnerability_scan.go
Original file line number Diff line number Diff line change
@@ -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, ",")
}
Loading

0 comments on commit e33cd12

Please sign in to comment.