Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vulnerability Scanning Implementation for container images #1489

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ jobs:
go-version: '1.21.x'
cache: true
check-latest: true
- name: Install Trivy
run: make install-trivy
- name: Build
run: make build
- name: Test
Expand Down Expand Up @@ -119,6 +121,7 @@ jobs:
# Build and load the Git and Bundle image
export GIT_CONTAINER_IMAGE="$(KO_DOCKER_REPO=kind.local ko publish ./cmd/git)"
export BUNDLE_CONTAINER_IMAGE="$(KO_DOCKER_REPO=kind.local ko publish ./cmd/bundle)"
export IMAGE_PROCESSING_CONTAINER_IMAGE="$(KO_DOCKER_REPO=kind.local ko publish ./cmd/image-processing)"

make test-integration

Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@ install-counterfeiter:
install-spruce:
hack/install-spruce.sh

.PHONY: install-trivy
install-trivy:
hack/install-trivy.sh

# Install golangci-lint via: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
.PHONY: sanity-check
sanity-check:
Expand Down
70 changes: 68 additions & 2 deletions cmd/image-processing/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,20 @@ package main

import (
"context"
"errors"
"fmt"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"time"

"github.com/google/go-containerregistry/pkg/name"
containerreg "github.com/google/go-containerregistry/pkg/v1"
buildapi "github.com/shipwright-io/build/pkg/apis/build/v1beta1"
karanibm6 marked this conversation as resolved.
Show resolved Hide resolved
"github.com/shipwright-io/build/pkg/image"
"github.com/shipwright-io/build/pkg/reconciler/buildrun/resources"
"github.com/spf13/pflag"
)

Expand All @@ -45,7 +49,10 @@ type settings struct {
imageTimestampFile,
resultFileImageDigest,
resultFileImageSize,
resultFileImageVulnerabilities,
secretPath string
vulnerabilitySettings resources.VulnerablilityScanParams
vulnerabilityCountLimit int
}

var flagValues settings
Expand All @@ -70,6 +77,9 @@ func initializeFlag() {

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 json string. One can enable the scan by setting {\"enabled\":true} to this option")
pflag.IntVar(&flagValues.vulnerabilityCountLimit, "vuln-count-limit", 50, "vulnerability count limit for the output of vulnerability scan")
}

func main() {
Expand Down Expand Up @@ -143,20 +153,21 @@ func runImageProcessing(ctx context.Context) error {
}

// prepare the registry options
options, _, err := image.GetOptions(ctx, imageName, flagValues.insecure, flagValues.secretPath, "Shipwright Build")
options, auth, err := image.GetOptions(ctx, imageName, flagValues.insecure, flagValues.secretPath, "Shipwright Build")
if err != nil {
return err
}

// load the image or image index (usually multi-platform image)
var img containerreg.Image
var imageIndex containerreg.ImageIndex
var isImageFromTar bool
if flagValues.push == "" {
log.Printf("Loading the image from the registry %q\n", imageName.String())
img, imageIndex, err = image.LoadImageOrImageIndexFromRegistry(imageName, options)
} else {
log.Printf("Loading the image from the directory %q\n", flagValues.push)
img, imageIndex, err = image.LoadImageOrImageIndexFromDirectory(flagValues.push)
img, imageIndex, isImageFromTar, err = image.LoadImageOrImageIndexFromDirectory(flagValues.push)
}
if err != nil {
log.Printf("Failed to load the image: %v\n", err)
Expand All @@ -179,6 +190,53 @@ func runImageProcessing(ctx context.Context) error {
}
}

// check for image vulnerabilities if vulnerability scanning is enabled.
var vulns []buildapi.Vulnerability

if flagValues.vulnerabilitySettings.Enabled {
var imageString string
var imageInDir bool
if flagValues.push != "" {
imageString = flagValues.push

// for single image in a tar file
if isImageFromTar {
entries, err := os.ReadDir(flagValues.push)
if err != nil {
return err
}
imageString = filepath.Join(imageString, entries[0].Name())
}
imageInDir = true
} else {
imageString = imageName.String()
imageInDir = false
}
vulns, err = image.RunVulnerabilityScan(ctx, imageString, flagValues.vulnerabilitySettings.VulnerabilityScanOptions, auth, flagValues.insecure, imageInDir, flagValues.vulnerabilityCountLimit)
if err != nil {
return err
}

// log all the vulnerabilities
if len(vulns) > 0 {
log.Println("vulnerabilities found in the output image :")
for _, vuln := range vulns {
log.Printf("ID: %s, Severity: %s\n", vuln.ID, vuln.Severity)
}
}
vulnOuput := serializeVulnerabilities(vulns)
if err := os.WriteFile(flagValues.resultFileImageVulnerabilities, vulnOuput, 0640); err != nil {
return err
}
}

// Don't push the image if fail is set to true for shipwright managed push
if flagValues.push != "" {
if flagValues.vulnerabilitySettings.FailOnFinding && len(vulns) > 0 {
log.Println("vulnerabilities have been found in the output image, exiting with code 22")
return &ExitError{Code: 22, Message: "vulnerabilities found, exiting with code 22", Cause: errors.New("vulnerabilities found in the image")}
}
}
// mutate the image timestamp
if flagValues.imageTimestamp != "" {
sec, err := strconv.ParseInt(flagValues.imageTimestamp, 10, 32)
Expand Down Expand Up @@ -234,3 +292,11 @@ func splitKeyVals(kvPairs []string) (map[string]string, error) {

return m, nil
}

func serializeVulnerabilities(Vulnerabilities []buildapi.Vulnerability) []byte {
var output []string
for _, vuln := range Vulnerabilities {
output = append(output, fmt.Sprintf("%s:%c", vuln.ID, vuln.Severity[0]))
}
return []byte(strings.Join(output, ","))
}
143 changes: 143 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"
"time"

. "github.com/onsi/ginkgo/v2"
Expand All @@ -24,6 +26,8 @@ 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"
buildapi "github.com/shipwright-io/build/pkg/apis/build/v1beta1"
"github.com/shipwright-io/build/pkg/reconciler/buildrun/resources"
"github.com/spf13/pflag"
"k8s.io/apimachinery/pkg/util/rand"
)
Expand Down Expand Up @@ -399,4 +403,143 @@ var _ = Describe("Image Processing Resource", func() {
})
})
})

Context("vulnerability scanning", func() {
var directory string
BeforeEach(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 and output vulnerabilities equal to the limit defined", func() {
vulnOptions := &buildapi.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())
vulnSettings := &resources.VulnerablilityScanParams{VulnerabilityScanOptions: *vulnOptions}
withTempFile("vuln-scan-result", func(filename string) {
Expect(run(
"--insecure",
"--image", tag.String(),
"--push", directory,
"--vuln-settings", vulnSettings.String(),
"--result-file-image-vulnerabilities", filename,
"--vuln-count-limit", "10",
)).ToNot(HaveOccurred())
output := filecontent(filename)
Expect(output).To(ContainSubstring("CVE-2019-8457"))
vulnerabilities := strings.Split(output, ",")
Expect(vulnerabilities).To(HaveLen(10))
})
})
})

It("should push the image if vulnerabilities are found and fail is false", func() {
vulnOptions := &buildapi.VulnerabilityScanOptions{
Enabled: true,
FailOnFinding: false,
}
withTempRegistry(func(endpoint string) {
tag, err := name.NewTag(fmt.Sprintf("%s/%s:%s", endpoint, "temp-image", rand.String(5)))
Expect(err).ToNot(HaveOccurred())
vulnSettings := &resources.VulnerablilityScanParams{VulnerabilityScanOptions: *vulnOptions}
withTempFile("vuln-scan-result", func(filename string) {
Expect(run(
"--insecure",
"--image", tag.String(),
"--push", directory,
"--vuln-settings", vulnSettings.String(),
"--result-file-image-vulnerabilities", filename,
)).ToNot(HaveOccurred())
output := filecontent(filename)
Expect(output).To(ContainSubstring("CVE-2019-8457"))
})

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 fail is true", func() {
vulnOptions := &buildapi.VulnerabilityScanOptions{
Enabled: true,
FailOnFinding: true,
}
withTempRegistry(func(endpoint string) {
tag, err := name.NewTag(fmt.Sprintf("%s/%s:%s", endpoint, "temp-image", rand.String(5)))
Expect(err).ToNot(HaveOccurred())
vulnSettings := &resources.VulnerablilityScanParams{VulnerabilityScanOptions: *vulnOptions}
withTempFile("vuln-scan-result", func(filename string) {
Expect(run(
"--insecure",
"--image", tag.String(),
"--push", directory,
"--vuln-settings", vulnSettings.String(),
"--result-file-image-vulnerabilities", filename,
)).To(HaveOccurred())
output := filecontent(filename)
Expect(output).To(ContainSubstring("CVE-2019-8457"))
})

ref, err := name.ParseReference(tag.String())
Expect(err).ToNot(HaveOccurred())

_, err = remote.Get(ref)
Expect(err).To(HaveOccurred())

})
})

It("should run vulnerability scanning on an image that is already pushed by the strategy", func() {
ignoreVulnerabilities := buildapi.IgnoredHigh
vulnOptions := &buildapi.VulnerabilityScanOptions{
Enabled: true,
FailOnFinding: true,
Ignore: &buildapi.VulnerabilityIgnoreOptions{
Severity: &ignoreVulnerabilities,
},
}

withTempRegistry(func(endpoint string) {
originalImageRef := "ghcr.io/shipwright-io/shipwright-samples/node:12"
srcRef, err := name.ParseReference(originalImageRef)
Expect(err).ToNot(HaveOccurred())

// Pull the original image
originalImage, err := remote.Image(srcRef)
Expect(err).ToNot(HaveOccurred())

// Tag the image with a new name
tag, err := name.NewTag(fmt.Sprintf("%s/%s:%s", endpoint, "temp-image", rand.String(5)))
Expect(err).ToNot(HaveOccurred())

err = remote.Write(tag, originalImage)
Expect(err).ToNot(HaveOccurred())

vulnSettings := &resources.VulnerablilityScanParams{VulnerabilityScanOptions: *vulnOptions}
withTempFile("vuln-scan-result", func(filename string) {
Expect(run(
"--insecure",
"--image", tag.String(),
"--vuln-settings", vulnSettings.String(),
"--result-file-image-vulnerabilities", filename,
)).ToNot(HaveOccurred())
output := filecontent(filename)
Expect(output).To(ContainSubstring("CVE-2019-12900"))
})

ref, err := name.ParseReference(tag.String())
Expect(err).ToNot(HaveOccurred())

_, err = remote.Get(ref)
Expect(err).ToNot(HaveOccurred())
})
})

})
})
Loading