Skip to content

Commit

Permalink
Merge pull request #1489 from karanibm6/add-vulnerability-scanning
Browse files Browse the repository at this point in the history
Vulnerability Scanning Implementation for container images
  • Loading branch information
openshift-merge-bot[bot] authored Jun 10, 2024
2 parents 0aa11b8 + 477214a commit 977a065
Show file tree
Hide file tree
Showing 91 changed files with 2,093 additions and 40 deletions.
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"
"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

0 comments on commit 977a065

Please sign in to comment.