How To Check Docker Images for Vulnerabilities
Join the DZone community and get the full member experience.
Join For FreeRegularly 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 do you keep up with this? You are mainly focused on business and do not want to have a full-time job 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 such as Java JAR files for vulnerabilities and will report them. This way, you have a great tool that 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. In the end, you will learn how to add the scan to your Jenkins pipeline.
The sources used in this blog can be found on GitHub.
2. Prerequisites
The prerequisites needed for this blog are:
- Basic Linux knowledge
- Basic Docker knowledge
- Basic Java and Spring Boot knowledge
3. Vulnerable Application
Navigate to Spring Initializr and choose 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 to 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 packageINSTALLED
: Which version is installedFIXED-IN
: In which version the vulnerability is fixedTYPE
: The type of 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 databaseSEVERITY
: Speaks for itself and can be negligible, low, medium, high, or critical.
As you take a closer look at the output, you will notice that not every vulnerability has a confirmed fix. So what do you 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 to 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 do you 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 is 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. Add the following section to the pom:
<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 a CI platform.
The first question to answer is how you will be notified when vulnerabilities are found. Up until 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 execute the grype Docker scan from within Maven. There is no separate 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 failures.
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.
Published at DZone with permission of Gunter Rotsaert, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments