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.
Should use the property, `snakeyaml.version`, to override instead of the way you’re describing. https://docs.spring.io/spring-boot/docs/current/reference/html/dependency-versions.html
LikeLike
Thank you very much for this addition, you are absolutely right. I added it as the preferred solution for this case and gave you credits for it.
LikeLike