Regularily checking for vulnerabilities in your pipeline is very important. One of the steps to execute is to perform a vulnerability scan of your Docker images. In this blog, you will learn how to perform the vulnerability scan, how to fix the vulnerabilities and how to add it to your Jenkins pipeline. Enjoy!

1. Introduction

In a previous blog from a few years ago, it was described how you could scan your Docker images for vulnerabilities. A follow-up blog showed how to add the scan to a Jenkins pipeline. However, Anchore Engine, which was used in the previous blogs, is not supported anymore. An alternative solution is available with grype which is also provided by Anchore. In this blog, you will take a closer look at grype, how it works, how you can fix the issues and how you can add it to your Jenkins pipeline.

But first of all, why check for vulnerabilities? You have to stay up-to-date with the latest security fixes nowadays. Many security vulnerabilities are publicly available and therefore can be exploited quite easily. It is therefore a must have to fix security vulnerabilities as fast as possible in order to minimize your attack surface. But how to keep up with this? Your are mainly focussed at the business and do not want to have a full-time job at fixing security vulnerabilities. That is why it is important to scan your application and your Docker images automatically. Grype can help with scanning your Docker images. Grype will check Operating System vulnerabilities but also language specific packages, like Java jar files, for vulnerabilities and will report them. This way, you have a great tool which will automate the security checks for you. Do note that grype is not limited to scanning Docker images. It can also scan files and directories and can therefore be used for scanning your sources.

In this blog, you will create a vulnerable Docker image containing a Spring Boot application. You will install and use grype in order to scan the image and fix the vulnerabilities. At the end, you will learn how to add the scan to your Jenkins pipeline.

The sources used in this blog can be found at GitHub.

2. Prerequisites

Prerequisites needed for this blog are:

  • Basic Linux knowledge;
  • Basic Docker knowlegde;
  • Basic Java and Spring Boot knowledge.

3. Vulnerable Application

Navigate to Spring Initializr and choose for a Maven build, Java 17, Spring Boot 2.7.6 and the Spring Web dependency. This will not be a very vulnerable application because Spring already ensures that you use the latest Spring Boot version. Therefore, change the Spring Boot version into 2.7.0. The Spring Boot application can be built with the following command, which will create the jar file for you:

$ mvn clean verify

You are going to scan a Docker image, therefore a Dockerfile needs to be created. You will use a very basic Dockerfile which just contains the minimum instructions needed to create the image. If you want to create production ready Docker images, do read the posts Docker Best Practices and Spring Boot Docker Best Practices.

FROM eclipse-temurin:17.0.1_12-jre-alpine
WORKDIR /opt/app
ARG JAR_FILE
COPY target/${JAR_FILE} app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

At the time of writing, the latest eclipse-temurin base image for Java 17 is version 17.0.5_8. Again, use an older one in order to make it vulnerable.

For building the Docker image, a fork of the dockerfile-maven-plugin of Spotify will be used. The following snippet is therefore added to the pom file.

<plugin>
  <groupId>com.xenoamess.docker</groupId>
  <artifactId>dockerfile-maven-plugin</artifactId>
  <version>1.4.25</version>
  <configuration>
    <repository>mydeveloperplanet/mygrypeplanet</repository>
    <tag>${project.version}</tag>
    <buildArgs>
      <JAR_FILE>${project.build.finalName}.jar</JAR_FILE>
    </buildArgs>
  </configuration>
</plugin>

The advantage of using this plugin is that you can easily reuse the configuration. Creating the Docker image can be done by a single Maven command.

Building the Docker image can be done by invoking the following command:

$ mvn dockerfile:build

You are now all set up to get started with grype.

4. Installation

Installation of grype can be done by executing the following script:

$ curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin

Verify the installation by executing the following command:

$ grype version
Application:          grype
Version:              0.54.0
Syft Version:         v0.63.0
BuildDate:            2022-12-13T15:02:51Z
GitCommit:            93499eec7e3ce2704755e9f51457181b06b519c5
GitDescription:       v0.54.0
Platform:             linux/amd64
GoVersion:            go1.18.8
Compiler:             gc
Supported DB Schema:  5

5. Scan Image

Scanning the Docker image is done by calling grype followed by docker:, indicating that you want to scan an image from the Docker daemon, the image and the tag:

$ grype docker:mydeveloperplanet/mygrypeplanet:0.0.1-SNAPSHOT
Application:          grype
Version:              0.54.0
Syft Version:         v0.63.0
 Vulnerability DB        [updated]
 Loaded image            
 Parsed image            
 Cataloged packages      [50 packages]
 Scanned image           [42 vulnerabilities]
NAME              INSTALLED  FIXED-IN   TYPE          VULNERABILITY        SEVERITY 
busybox           1.34.1-r3  1.34.1-r5  apk           CVE-2022-28391       High      
jackson-databind  2.13.3                java-archive  CVE-2022-42003       High      
jackson-databind  2.13.3                java-archive  CVE-2022-42004       High      
jackson-databind  2.13.3     2.13.4     java-archive  GHSA-rgv9-q543-rqg4  High      
jackson-databind  2.13.3     2.13.4.1   java-archive  GHSA-jjjh-jjxp-wpff  High      
java              17.0.1+12             binary        CVE-2022-21248       Low       
java              17.0.1+12             binary        CVE-2022-21277       Medium    
java              17.0.1+12             binary        CVE-2022-21282       Medium    
java              17.0.1+12             binary        CVE-2022-21283       Medium    
java              17.0.1+12             binary        CVE-2022-21291       Medium    
java              17.0.1+12             binary        CVE-2022-21293       Medium    
java              17.0.1+12             binary        CVE-2022-21294       Medium    
java              17.0.1+12             binary        CVE-2022-21296       Medium    
java              17.0.1+12             binary        CVE-2022-21299       Medium    
java              17.0.1+12             binary        CVE-2022-21305       Medium    
java              17.0.1+12             binary        CVE-2022-21340       Medium    
java              17.0.1+12             binary        CVE-2022-21341       Medium    
java              17.0.1+12             binary        CVE-2022-21360       Medium    
java              17.0.1+12             binary        CVE-2022-21365       Medium    
java              17.0.1+12             binary        CVE-2022-21366       Medium    
libcrypto1.1      1.1.1l-r7             apk           CVE-2021-4160        Medium    
libcrypto1.1      1.1.1l-r7  1.1.1n-r0  apk           CVE-2022-0778        High      
libcrypto1.1      1.1.1l-r7  1.1.1q-r0  apk           CVE-2022-2097        Medium    
libretls          3.3.4-r2   3.3.4-r3   apk           CVE-2022-0778        High      
libssl1.1         1.1.1l-r7             apk           CVE-2021-4160        Medium    
libssl1.1         1.1.1l-r7  1.1.1n-r0  apk           CVE-2022-0778        High      
libssl1.1         1.1.1l-r7  1.1.1q-r0  apk           CVE-2022-2097        Medium    
snakeyaml         1.30                  java-archive  GHSA-mjmj-j48q-9wg2  High      
snakeyaml         1.30       1.31       java-archive  GHSA-3mc7-4q67-w48m  High      
snakeyaml         1.30       1.31       java-archive  GHSA-98wm-3w3q-mw94  Medium    
snakeyaml         1.30       1.31       java-archive  GHSA-c4r9-r8fh-9vj2  Medium    
snakeyaml         1.30       1.31       java-archive  GHSA-hhhw-99gj-p3c3  Medium    
snakeyaml         1.30       1.32       java-archive  GHSA-9w3m-gqgf-c4p9  Medium    
snakeyaml         1.30       1.32       java-archive  GHSA-w37g-rhq8-7m4j  Medium    
spring-core       5.3.20                java-archive  CVE-2016-1000027     Critical  
ssl_client        1.34.1-r3  1.34.1-r5  apk           CVE-2022-28391       High      
zlib              1.2.11-r3  1.2.12-r0  apk           CVE-2018-25032       High      
zlib              1.2.11-r3  1.2.12-r2  apk           CVE-2022-37434       Critical 

What does this output tell you?

  • NAME: The name of the vulnerable package;
  • INSTALLED: Which version is installed;
  • FIXED-IN: In which version the vulnerability is fixed;
  • TYPE: The type of the dependency, e.g. binary for the JDK, etc;
  • VULNERABILITY; The identifier of the vulnerability. With this identifier, you are able to get more information about the vulnerability in the CVE database;
  • SEVERITY: Speaks for itself and can be one of negligible, low, medium, high, critical.

As you take a closer look at the output, you will notice that not every vulnerability has a confirmed fix. So what to do in that case? Grype provides an option in order to show only the vulnerabilities with a confirmed fix. Adding the --only-fixed flag will do the trick.

$ grype docker:mydeveloperplanet/mygrypeplanet:0.0.1-SNAPSHOT --only-fixed
 Vulnerability DB        [no update available]
 Loaded image            
 Parsed image            
 Cataloged packages      [50 packages]
 Scanned image           [42 vulnerabilities]

NAME              INSTALLED  FIXED-IN   TYPE          VULNERABILITY        SEVERITY 
busybox           1.34.1-r3  1.34.1-r5  apk           CVE-2022-28391       High      
jackson-databind  2.13.3     2.13.4     java-archive  GHSA-rgv9-q543-rqg4  High      
jackson-databind  2.13.3     2.13.4.1   java-archive  GHSA-jjjh-jjxp-wpff  High      
libcrypto1.1      1.1.1l-r7  1.1.1n-r0  apk           CVE-2022-0778        High      
libcrypto1.1      1.1.1l-r7  1.1.1q-r0  apk           CVE-2022-2097        Medium    
libretls          3.3.4-r2   3.3.4-r3   apk           CVE-2022-0778        High      
libssl1.1         1.1.1l-r7  1.1.1n-r0  apk           CVE-2022-0778        High      
libssl1.1         1.1.1l-r7  1.1.1q-r0  apk           CVE-2022-2097        Medium    
snakeyaml         1.30       1.31       java-archive  GHSA-3mc7-4q67-w48m  High      
snakeyaml         1.30       1.31       java-archive  GHSA-98wm-3w3q-mw94  Medium    
snakeyaml         1.30       1.31       java-archive  GHSA-c4r9-r8fh-9vj2  Medium    
snakeyaml         1.30       1.31       java-archive  GHSA-hhhw-99gj-p3c3  Medium    
snakeyaml         1.30       1.32       java-archive  GHSA-9w3m-gqgf-c4p9  Medium    
snakeyaml         1.30       1.32       java-archive  GHSA-w37g-rhq8-7m4j  Medium    
ssl_client        1.34.1-r3  1.34.1-r5  apk           CVE-2022-28391       High      
zlib              1.2.11-r3  1.2.12-r0  apk           CVE-2018-25032       High      
zlib              1.2.11-r3  1.2.12-r2  apk           CVE-2022-37434       Critical  

Note that the vulnerabilities for the Java JDK have disappeared, although there exists a more recent update for the Java 17 JDK. However, this might not be a big issue, because the other (non java-archive) vulnerabilities show you that the base image is outdated.

6. Fix Vulnerabilities

Fixing the vulnerabilities is quite easy in this case. First of all, you need to update the Docker base image. Change the first line in the Docker image:

FROM eclipse-temurin:17.0.1_12-jre-alpine

into:

FROM eclipse-temurin:17.0.5_8-jre-alpine

Build the image and run the scan again:

$ mvn dockerfile:build
...
$ grype docker:mydeveloperplanet/mygrypeplanet:0.0.1-SNAPSHOT --only-fixed
 Vulnerability DB        [no update available]
 Loaded image            
 Parsed image            
 Cataloged packages      [62 packages]
 Scanned image           [14 vulnerabilities]
NAME              INSTALLED  FIXED-IN  TYPE          VULNERABILITY        SEVERITY 
jackson-databind  2.13.3     2.13.4    java-archive  GHSA-rgv9-q543-rqg4  High      
jackson-databind  2.13.3     2.13.4.1  java-archive  GHSA-jjjh-jjxp-wpff  High      
snakeyaml         1.30       1.31      java-archive  GHSA-3mc7-4q67-w48m  High      
snakeyaml         1.30       1.31      java-archive  GHSA-98wm-3w3q-mw94  Medium    
snakeyaml         1.30       1.31      java-archive  GHSA-c4r9-r8fh-9vj2  Medium    
snakeyaml         1.30       1.31      java-archive  GHSA-hhhw-99gj-p3c3  Medium    
snakeyaml         1.30       1.32      java-archive  GHSA-9w3m-gqgf-c4p9  Medium    
snakeyaml         1.30       1.32      java-archive  GHSA-w37g-rhq8-7m4j  Medium 

As you can see in the output, only the java-archive vulnerabilities are still present. The other vulnerabilities have been solved.

Next, fix the Spring Boot dependency vulnerability. Change the version of Spring Boot from 2.7.0 into 2.7.6 in the pom.

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.7.6</version>
  <relativePath/> <!-- lookup parent from repository -->
</parent>

Build the jar file, build the Docker image and run the scan again:

$ mvn clean verify
...
$ mvn dockerfile:build
...
$ grype docker:mydeveloperplanet/mygrypeplanet:0.0.1-SNAPSHOT --only-fixed
 Vulnerability DB        [no update available]
 Loaded image            
 Parsed image            
 Cataloged packages      [62 packages]
 Scanned image           [10 vulnerabilities]
NAME       INSTALLED  FIXED-IN  TYPE          VULNERABILITY        SEVERITY 
snakeyaml  1.30       1.31      java-archive  GHSA-3mc7-4q67-w48m  High      
snakeyaml  1.30       1.31      java-archive  GHSA-98wm-3w3q-mw94  Medium    
snakeyaml  1.30       1.31      java-archive  GHSA-c4r9-r8fh-9vj2  Medium    
snakeyaml  1.30       1.31      java-archive  GHSA-hhhw-99gj-p3c3  Medium    
snakeyaml  1.30       1.32      java-archive  GHSA-9w3m-gqgf-c4p9  Medium    
snakeyaml  1.30       1.32      java-archive  GHSA-w37g-rhq8-7m4j  Medium 

So, you got rid of the jackson-databind vulnerability, but not of the snakeyaml vulnerability. So, in which dependency is snakeyaml 1.30 being used? You can find out by means of the dependency:tree Maven command. For brevity purposes, only a part of the output is shown here:

$ mvnd dependency:tree
...
 com.mydeveloperplanet:mygrypeplanet:jar:0.0.1-SNAPSHOT
[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:2.7.6:compile
[INFO] |  +- org.springframework.boot:spring-boot-starter:jar:2.7.6:compile
[INFO] |  |  +- org.springframework.boot:spring-boot:jar:2.7.6:compile
[INFO] |  |  +- org.springframework.boot:spring-boot-autoconfigure:jar:2.7.6:compile
[INFO] |  |  +- org.springframework.boot:spring-boot-starter-logging:jar:2.7.6:compile
[INFO] |  |  |  +- ch.qos.logback:logback-classic:jar:1.2.11:compile
[INFO] |  |  |  |  \- ch.qos.logback:logback-core:jar:1.2.11:compile
[INFO] |  |  |  +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.17.2:compile
[INFO] |  |  |  |  \- org.apache.logging.log4j:log4j-api:jar:2.17.2:compile
[INFO] |  |  |  \- org.slf4j:jul-to-slf4j:jar:1.7.36:compile
[INFO] |  |  +- jakarta.annotation:jakarta.annotation-api:jar:1.3.5:compile
[INFO] |  |  \- org.yaml:snakeyaml:jar:1.30:compile
...

The output shows us that the dependency is part of the spring-boot-starter-web dependency. So, how to solve this? Strictly speaking, Spring has to solve it. But if you do not want to wait for a solution, you can solve it by yourself.

Solution 1: you do not need the dependency. This is the easiest fix and low risk. Just exclude the dependency from the spring-boot-starter-web dependency in the pom.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
  <exclusions>
    <exclusion>
	  <groupId>org.yaml</groupId>
	  <artifactId>snakeyaml</artifactId>
	</exclusion>
  </exclusions>
</dependency>

Build the jar file, build the Docker image and run the scan again:

$ mvn clean verify
...
$ mvn dockerfile:build
...
$ grype docker:mydeveloperplanet/mygrypeplanet:0.0.1-SNAPSHOT --only-fixed
 Vulnerability DB        [no update available]
 Loaded image            
 Parsed image            
 Cataloged packages      [61 packages]
 Scanned image           [3 vulnerabilities]
No vulnerabilities found

No vulnerabilities are found anymore.

Solution 2: you do need the dependency. You can replace this transitive dependency by means of dependencyManagement in the pom. This is a bit more tricky, because the updated transitive dependency is not tested with the spring-boot-starter-web dependency. It is a trade-off whether you want to do this or not. Upgrade the version of the snakeyaml dependency, see here for the full list of available versions (thanks to zimeron for this addition). Add the updated version to the properties section.

<properties>
  <java.version>17</java.version>
  <snakeyaml.version>1.32</snakeyaml.version>
</properties>

When it is not possible to change the version as mentioned above, the following alternative can be used:

<dependencyManagement>
  <dependencies>
    <dependency>
	  <groupId>org.yaml</groupId>
	  <artifactId>snakeyaml</artifactId>
	  <version>1.32</version>
	</dependency>
  </dependencies>
</dependencyManagement>

Build the jar file, build the Docker image and run the scan again:

$ mvn clean verify
...
$ mvn dockerfile:build
...
$ grype docker:mydeveloperplanet/mygrypeplanet:0.0.1-SNAPSHOT --only-fixed
 Vulnerability DB        [no update available]
 Loaded image            
 Parsed image            
 Cataloged packages      [62 packages]
 Scanned image           [3 vulnerabilities]
No vulnerabilities found

Again, no vulnerabilities are present anymore.

Solution 3: this is the solution when you do not want to do anything or whether it is a false positive notification. Create a .grype.yaml file where you exclude the vulnerability with High severity and execute the scan with the --config flag followed by the .grype.yaml file containing the exclusions.

The .grype.yaml file looks as follows:

ignore:
  - vulnerability: GHSA-3mc7-4q67-w48m

Run the scan again:

$ grype docker:mydeveloperplanet/mygrypeplanet:0.0.1-SNAPSHOT --only-fixed
 Vulnerability DB        [no update available]
 Loaded image            
 Parsed image            
 Cataloged packages      [62 packages]
 Scanned image           [10 vulnerabilities]
NAME       INSTALLED  FIXED-IN  TYPE          VULNERABILITY        SEVERITY 
snakeyaml  1.30       1.31      java-archive  GHSA-98wm-3w3q-mw94  Medium    
snakeyaml  1.30       1.31      java-archive  GHSA-c4r9-r8fh-9vj2  Medium    
snakeyaml  1.30       1.31      java-archive  GHSA-hhhw-99gj-p3c3  Medium    
snakeyaml  1.30       1.32      java-archive  GHSA-9w3m-gqgf-c4p9  Medium    
snakeyaml  1.30       1.32      java-archive  GHSA-w37g-rhq8-7m4j  Medium 

The High vulnerability is not shown anymore.

7. Continuous Integration

Now you know how to manually scan your Docker images. However, you probably want to scan the images as part of your continuous integration pipeline. In this section, a solution is provided when using Jenkins as CI platform.

First question to answer is how you will be notified when vulnerabilities are found? Up till now, you only noticed the vulnerabilities by looking at the standard output. This is not a solution for a CI pipeline. You want to get notified and this can be done by failing the build. Grype has the --fail-on <severity> flag for this purpose. You probably do not want to fail the pipeline when a vulnerability with severity negligible has been found.

Let’s see what happens when you execute this manually. First of all, introduce the vulnerabilities again in the Spring Boot application and in the Docker image.

Build the jar file, build the Docker image and run the scan with flag --fail-on:

$ mvn clean verify
...
$ mvn dockerfile:build
...
$ grype docker:mydeveloperplanet/mygrypeplanet:0.0.1-SNAPSHOT --only-fixed --fail-on high
...
1 error occurred:
        * discovered vulnerabilities at or above the severity threshold

Not all output has been shown here, but only the important part. And, as you can see, at the end of the output, a message is shown that the scan has generated an error. This will cause your Jenkins pipeline to fail and as a consequence, the developers are notified that something went wrong.

In order to add this to your Jenkins pipeline, several options exist. Here it is chosen to create the Docker image and to execute the grype Docker scan from within Maven. There is no seperate Maven plugin for grype, but you can use the exec-maven-plugin for this purpose. Add the following to the build-plugins section of the pom.

<build>
  <plugins>
    <plugin>
      <groupId>org.codehaus.mojo</groupId>
      <artifactId>exec-maven-plugin</artifactId>
      <version>3.1.0</version>
      <configuration>
        <executable>grype</executable>
    	  <arguments>
    	    <argument>docker:mydeveloperplanet/mygrypeplanet:${project.version}</argument>
	        <argument>--scope</argument>
	        <argument>all-layers</argument>
	        <argument>--fail-on</argument>
	        <argument>high</argument>
	        <argument>--only-fixed</argument>
	        <argument>-q</argument>
	      </arguments>
      </configuration>
    </plugin>
  </plugins>
</build>

Two extra flags are added here:

  • --scope all-layers: This will scan all layers involved in the Docker image;
  • -q: This will use quiet logging and will show only the vulnerabilities and possible failure.

You can invoke this with the following command:

$ mvnd exec:exec

You can add this to your Jenkinsfile inside the withMaven wrapper:

withMaven() {
  sh 'mvn dockerfile:build dockerfile:push exec:exec'
}

8. Conclusion

In this blog, you learned how to scan your Docker images by means of grype. Grype has some interesting, user-friendly features which allow you to efficiently add them to your Jenkins pipeline. Also, installing grype is quite easy. Grype is definitely a great improvement over Anchor Engine.