diff --git a/.gitignore b/.gitignore
index 0b14f1fd..5a29379a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@
/*.iml
/.idea/
.mvn/**
+**/trivyReport.json
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 038b0815..845e0f5a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+- Add Trivy class for scanning container images with Trivy
+ - Combines the functionality of the findVulnerabilitiesWithTrivy function and the Trivy class of the dogu-build-lib
+
+### Deprecated
+- findVulnerabilitiesWithTrivy function is deprecated now. Please use the new Trivy class.
+
### Changed
- [#140] Update Maven-Build-Dependencies
- JUnit 5
diff --git a/Jenkinsfile b/Jenkinsfile
index 19107473..2917e5e6 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -11,7 +11,7 @@ node('docker') {
def cesBuildLib = libraryFromLocalRepo().com.cloudogu.ces.cesbuildlib
- def mvn = cesBuildLib.MavenWrapperInDocker.new(this, 'adoptopenjdk/openjdk11:jdk-11.0.10_9-alpine')
+ def mvn = cesBuildLib.MavenWrapperInDocker.new(this, 'eclipse-temurin:11.0.25_9-jdk-alpine')
mvn.useLocalRepoFromJenkins = true
def git = cesBuildLib.Git.new(this)
@@ -40,7 +40,7 @@ node('docker') {
}
stage('Unit Test') {
- mvn 'test -Dmaven.test.failure.ignore=true'
+ mvn 'test'
// Archive Unit and integration test results, if any
junit allowEmptyResults: true, testResults: '**/target/failsafe-reports/TEST-*.xml,**/target/surefire-reports/TEST-*.xml'
}
@@ -70,4 +70,4 @@ def libraryFromLocalRepo() {
// Checks out to workspace local folder named like the identifier.
// We have to pass an identifier with version (which is ignored). Otherwise the build fails.
library(identifier: 'ces-build-lib@snapshot', retriever: legacySCM(scm))
-}
\ No newline at end of file
+}
diff --git a/README.md b/README.md
index 052010eb..29ac16a3 100644
--- a/README.md
+++ b/README.md
@@ -68,6 +68,7 @@ Jenkins Pipeline Shared library, that contains additional features for Git, Mave
- [Markdown](#markdown)
- [DockerLint (Deprecated)](#dockerlint-deprecated)
- [ShellCheck](#shellcheck)
+- [Trivy](#trivy)
- [Steps](#steps)
- [mailIfStatusChanged](#mailifstatuschanged)
- [isPullRequest](#ispullrequest)
@@ -1240,6 +1241,179 @@ shellCheck(fileList) // fileList="a.sh b.sh" execute shellcheck on a custom list
See [shellCheck](vars/shellCheck.groovy)
+# Trivy
+
+Scan container images for vulnerabilities with Trivy.
+
+## Create a Trivy object
+
+```groovy
+Trivy trivy = new Trivy(this)
+// With specific Trivy version
+Trivy trivy = new Trivy(this, "0.57.1")
+// With specific Trivy image
+Trivy trivy = new Trivy(this, "0.57.1", "images.mycompany.test/trivy")
+// With explicit Docker registry
+Docker docker = new Docker(this)
+docker.withRegistry("https://my.registry.invalid", myRegistryCredentialsID)
+Trivy trivy = new Trivy(this, "0.57.1", "aquasec/trivy", docker)
+```
+
+## Scan image with Trivy
+
+Scan an image with Trivy by calling the `scanImage` function.
+
+```groovy
+Trivy trivy = new Trivy(this)
+boolean imageIsSafe = trivy.scanImage("ubuntu:24.04")
+if (!imageIsSafe){
+ echo "This image has vulnerabilities!"
+}
+```
+
+### Set the severity level for the scan
+
+You can set the severity levels of the vulnerabilities Trivy should scan for as a parameter of the scan method:
+
+```groovy
+Trivy trivy = new Trivy(this)
+trivy.scanImage("ubuntu:24.04", TrivySeverityLevel.ALL)
+trivy.scanImage("ubuntu:24.04", "CRITICAL,LOW")
+```
+
+For the available pre-defined severity levels see [TrivySeverityLevel.groovy](src/com/cloudogu/ces/cesbuildlib/TrivySeverityLevel.groovy)
+
+### Set the pipeline strategy
+
+To define how the Jenkins pipeline should behave if vulnerabilities are found, you can set certain strategies:
+- TrivyScanStrategy.IGNORE: Ignore the vulnerabilities and continue
+- TrivyScanStrategy.UNSTABLE: Mark the job as "unstable" and continue
+- TrivyScanStrategy.FAIL: Mark the job as failed
+
+```groovy
+Trivy trivy = new Trivy(this)
+trivy.scanImage("ubuntu:24.04", TrivySeverityLevel.ALL, TrivyScanStrategy.UNSTABLE)
+```
+
+### Set additional Trivy flags
+
+To set additional Trivy command flags, use the `additionalFlags` parameter:
+
+```groovy
+Trivy trivy = new Trivy(this)
+trivy.scanImage("ubuntu:24.04", TrivySeverityLevel.ALL, TrivyScanStrategy.UNSTABLE, "--db-repository public.ecr.aws/aquasecurity/trivy-db")
+```
+
+Note that the flags `--db-repository public.ecr.aws/aquasecurity/trivy-db --java-db-repository public.ecr.aws/aquasecurity/trivy-java-db`
+are set by default to avoid rate limiting of Trivy database downloads. If you set `additionalFlags` by yourself, you are overwriting
+these default flags and have to make sure to include them in your set of additional flags, if needed.
+
+### Set the Trivy report file name
+
+If you want to run multiple image scans in one pipeline, you can set distinct file names for the report files:
+
+```groovy
+Trivy trivy = new Trivy(this)
+trivy.scanImage("ubuntu:20.04", TrivySeverityLevel.ALL, TrivyScanStrategy.UNSTABLE, "", "trivy/ubuntu20.json")
+trivy.scanImage("ubuntu:24.04", TrivySeverityLevel.ALL, TrivyScanStrategy.UNSTABLE, "", "trivy/ubuntu24.json")
+// Save report by using the same file name (last parameter)
+trivy.saveFormattedTrivyReport(TrivyScanFormat.HTML, "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL", "ubuntu20.04report", "trivy/ubuntu20.json")
+```
+
+## Save Trivy report in another file format
+
+After calling the `scanImage` function you can save the scan report as JSON, HTML or table files.
+
+```groovy
+Trivy trivy = new Trivy(this)
+trivy.scanImage("ubuntu:24.04")
+trivy.saveFormattedTrivyReport(TrivyScanFormat.TABLE)
+trivy.saveFormattedTrivyReport(TrivyScanFormat.JSON)
+trivy.saveFormattedTrivyReport(TrivyScanFormat.HTML)
+```
+
+You may filter the output to show only specific severity levels (default: `"UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL"`):
+
+```groovy
+Trivy trivy = new Trivy(this)
+trivy.scanImage("ubuntu:24.04")
+trivy.saveFormattedTrivyReport(TrivyScanFormat.TABLE, "CRITICAL")
+trivy.saveFormattedTrivyReport(TrivyScanFormat.JSON, "UNKNOWN,LOW,MEDIUM")
+```
+
+You may also use any other supported [Trivy format](https://trivy.dev/v0.57/docs/references/configuration/cli/trivy_convert/) or a custom template from a file in your workspace.
+
+```groovy
+Trivy trivy = new Trivy(this)
+trivy.scanImage("ubuntu:24.04")
+trivy.saveFormattedTrivyReport("cosign-vuln", "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL", "ubuntu24.04cosign.txt")
+trivy.saveFormattedTrivyReport("template --template @myTemplateFile.xyz", "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL", "ubuntu24.04myTemplate.txt")
+```
+
+## Scan Dogu image with Trivy
+
+This section describes how to get a Dogu image from the testing CES instance and scan it with Trivy.
+
+### Get Dogu image from CES instance
+
+Make sure to have a `build` stage in your Dogu test pipeline which builds the Dogu image, e.g. via
+the `ecoSystem.build("/dogu")` command.
+After the build stage you will be able to copy the Dogu image to your local Jenkins worker via
+the `ecoSystem.copyDoguImageToJenkinsWorker("/dogu")` command.
+
+### Scan Dogu image
+
+The `scanDogu()` function lets you scan a Dogu image without typing its full name. The method reads the image name
+and version from the dogu.json inside the directory you point it to via its first argument.
+The default directory is the current directory.
+
+```groovy
+// Preparation
+ecoSystem.copyDoguImageToJenkinsWorker("/dogu")
+Trivy trivy = new Trivy(this)
+
+// Scan the Dogu image
+trivy.scanDogu()
+// Explicitly set directory that contains the dogu code (dogu.json)
+trivy.scanDogu("subfolder/test1/jenkins")
+// Set scan options just like in the scanImage method
+trivy.scanDogu(".", TrivySeverityLevel.ALL, TrivyScanStrategy.UNSTABLE, "", "trivy/mydogu.json")
+trivy.saveFormattedTrivyReport(TrivyScanFormat.TABLE)
+trivy.saveFormattedTrivyReport(TrivyScanFormat.JSON)
+trivy.saveFormattedTrivyReport(TrivyScanFormat.HTML)
+```
+
+## Ignore / allowlist
+
+If you want to ignore / allow certain vulnerabilities, please use a `.trivyignore` file.
+
+Provide the file in your repo `/` directory where you run your job, e.g.:
+
+```shell
+.gitignore
+Jenkinsfile
+.trivyignore
+```
+
+[Offical documentation](https://trivy.dev/v0.57/docs/configuration/filtering/#by-finding-ids)
+```ignorelang
+# Accept the risk
+CVE-2018-14618
+
+# Accept the risk until 2023-01-01
+CVE-2019-14697 exp:2023-01-01
+
+# No impact in our settings
+CVE-2019-1543
+
+# Ignore misconfigurations
+AVD-DS-0002
+
+# Ignore secrets
+generic-unwanted-rule
+aws-account-id
+```
+
# Steps
## mailIfStatusChanged
@@ -1293,7 +1467,9 @@ For example, if running on `http(s)://server:port/jenkins`, `server` is returned
Returns true if the build is successful, i.e. not failed or unstable (yet).
-## findVulnerabilitiesWithTrivy
+## findVulnerabilitiesWithTrivy (Deprecated)
+
+This function is deprecated. Use [Trivy](#trivy) functionality instead.
Returns a list of vulnerabilities or an empty list if there are no vulnerabilities for the given severity.
@@ -1330,36 +1506,7 @@ node {
}
```
-### Ignore / allowlist
-
-If you want to ignore / allow certain vulnerabilities please use a .trivyignore file
-Provide the file in your repo / directory where you run your job
-e.g.:
-```shell
-.gitignore
-Jenkinsfile
-.trivyignore
-```
-
-[Offical documentation](https://aquasecurity.github.io/trivy/v0.41/docs/configuration/filtering/#by-finding-ids)
-```ignorelang
-# Accept the risk
-CVE-2018-14618
-# Accept the risk until 2023-01-01
-CVE-2019-14697 exp:2023-01-01
-
-# No impact in our settings
-CVE-2019-1543
-
-# Ignore misconfigurations
-AVD-DS-0002
-
-# Ignore secrets
-generic-unwanted-rule
-aws-account-id
-
-```
If there are vulnerabilities the output looks as follows.
diff --git a/pom.xml b/pom.xml
index 2cecc83b..c7189b12 100644
--- a/pom.xml
+++ b/pom.xml
@@ -43,6 +43,12 @@
groovy-all
2.5.23
pom
+
+
+ org.codehaus.groovy
+ groovy-test
+
+
@@ -61,7 +67,14 @@
org.mockito
mockito-junit-jupiter
- 5.14.2
+ 3.6.28
+ test
+
+
+
+ org.mockito
+ mockito-core
+ 3.6.28
test
@@ -75,7 +88,14 @@
org.junit.jupiter
junit-jupiter
- 5.11.3
+ 5.4.2
+ test
+
+
+
+ org.junit.vintage
+ junit-vintage-engine
+ 5.4.2
test
@@ -86,6 +106,19 @@
test
+
+
+ commons-io
+ commons-io
+ 2.18.0
+ test
+
+
+ org.apache.commons
+ commons-compress
+ 1.27.1
+ test
+
diff --git a/src/com/cloudogu/ces/cesbuildlib/Docker.groovy b/src/com/cloudogu/ces/cesbuildlib/Docker.groovy
index 9937d91d..aeacce2f 100644
--- a/src/com/cloudogu/ces/cesbuildlib/Docker.groovy
+++ b/src/com/cloudogu/ces/cesbuildlib/Docker.groovy
@@ -401,7 +401,7 @@ class Docker implements Serializable {
def userName = sh.returnStdOut('whoami')
String jenkinsUserFromEtcPasswd = sh.returnStdOut "cat /etc/passwd | grep $userName"
- if (jenkinsUserFromEtcPasswd.isEmpty()) {
+ if (jenkinsUserFromEtcPasswd == null || jenkinsUserFromEtcPasswd.isEmpty()) {
script.error 'Unable to parse user jenkins from /etc/passwd.'
}
return jenkinsUserFromEtcPasswd
diff --git a/src/com/cloudogu/ces/cesbuildlib/Sh.groovy b/src/com/cloudogu/ces/cesbuildlib/Sh.groovy
index d9521c37..14f2e98c 100644
--- a/src/com/cloudogu/ces/cesbuildlib/Sh.groovy
+++ b/src/com/cloudogu/ces/cesbuildlib/Sh.groovy
@@ -18,6 +18,6 @@ class Sh implements Serializable {
// Trim to remove trailing line breaks, which result in unwanted behavior in Jenkinsfiles:
// E.g. when using output in other sh() calls leading to executing the sh command after the line breaks,
// possibly discarding additional arguments
- .trim()
+ ?.trim()
}
}
diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy
new file mode 100644
index 00000000..3e52fed5
--- /dev/null
+++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy
@@ -0,0 +1,162 @@
+package com.cloudogu.ces.cesbuildlib
+
+import com.cloudbees.groovy.cps.NonCPS
+
+class Trivy implements Serializable {
+ static final String DEFAULT_TRIVY_VERSION = "0.57.1"
+ static final String DEFAULT_TRIVY_IMAGE = "aquasec/trivy"
+ private script
+ private Docker docker
+ private String trivyVersion
+ private String trivyImage
+ private String trivyDirectory = "trivy"
+
+ // Do not use DEFAULT_TRIVY_VERSION or DEFAULT_TRIVY_IMAGE here, as it will lead to java.lang.VerifyError
+ Trivy(script, String trivyVersion = "0.57.1", String trivyImage = "aquasec/trivy", Docker docker = new Docker(script)) {
+ this.script = script
+ this.trivyVersion = trivyVersion
+ this.trivyImage = trivyImage
+ this.docker = docker
+ }
+
+ /**
+ * Scans an image for vulnerabilities.
+ * Notes:
+ * - Use a .trivyignore file for allowed CVEs
+ * - This function will generate a JSON formatted report file which can be converted to other formats via saveFormattedTrivyReport()
+ *
+ * @param imageName The name of the image to be scanned; may include a version tag
+ * @param severityLevel The vulnerability level to scan. Can be a member of TrivySeverityLevel or a custom String (e.g. 'CRITICAL,LOW')
+ * @param strategy The strategy to follow after the scan. Should the build become unstable or failed? Or Should any vulnerability be ignored? (@see TrivyScanStrategy)
+ * @param additionalFlags Additional Trivy command flags
+ * @param trivyReportFile Location of Trivy report file. Should be set individually when scanning multiple images in the same pipeline
+ * @return Returns true if the scan was ok (no vulnerability found); returns false if any vulnerability was found
+ */
+ boolean scanImage(
+ String imageName,
+ String severityLevel = TrivySeverityLevel.CRITICAL,
+ String strategy = TrivyScanStrategy.UNSTABLE,
+ // Avoid rate limits of default Trivy database source
+ String additionalFlags = "--db-repository public.ecr.aws/aquasecurity/trivy-db --java-db-repository public.ecr.aws/aquasecurity/trivy-java-db",
+ String trivyReportFile = "trivy/trivyReport.json"
+ ) {
+ Integer exitCode = docker.image("${trivyImage}:${trivyVersion}")
+ .mountJenkinsUser()
+ .mountDockerSocket()
+ .inside("-v ${script.env.WORKSPACE}/.trivy/.cache:/root/.cache/") {
+ // Write result to $trivyReportFile in json format (--format json), which can be converted in the saveFormattedTrivyReport function
+ // Exit with exit code 10 if vulnerabilities are found or OS is so old that Trivy has no records for it anymore
+ script.sh("mkdir -p " + trivyDirectory)
+ script.sh(script: "trivy image --exit-code 10 --exit-on-eol 10 --format ${TrivyScanFormat.JSON} -o ${trivyReportFile} --severity ${severityLevel} ${additionalFlags} ${imageName}", returnStatus: true)
+ }
+ switch (exitCode) {
+ case 0:
+ // Everything all right, no vulnerabilities
+ return true
+ case 10:
+ // Found vulnerabilities
+ // Set build status according to strategy
+ switch (strategy) {
+ case TrivyScanStrategy.IGNORE:
+ break
+ case TrivyScanStrategy.UNSTABLE:
+ script.archiveArtifacts artifacts: "${trivyReportFile}", allowEmptyArchive: true
+ script.unstable("Trivy has found vulnerabilities in image " + imageName + ". See " + trivyReportFile)
+ break
+ case TrivyScanStrategy.FAIL:
+ script.archiveArtifacts artifacts: "${trivyReportFile}", allowEmptyArchive: true
+ script.error("Trivy has found vulnerabilities in image " + imageName + ". See " + trivyReportFile)
+ break
+ }
+ return false
+ default:
+ script.error("Error during trivy scan; exit code: " + exitCode)
+ }
+ }
+
+ /**
+ * Scans a dogu image for vulnerabilities.
+ * Notes:
+ * - Use a .trivyignore file for allowed CVEs
+ * - This function will generate a JSON formatted report file which can be converted to other formats via saveFormattedTrivyReport()
+ *
+ * @param doguDir The directory the dogu code (dogu.json) is located
+ * @param severityLevel The vulnerability level to scan. Can be a member of TrivySeverityLevel or a custom String (e.g. 'CRITICAL,LOW')
+ * @param strategy The strategy to follow after the scan. Should the build become unstable or failed? Or Should any vulnerability be ignored? (@see TrivyScanStrategy)
+ * @param additionalFlags Additional Trivy command flags
+ * @param trivyReportFile Location of Trivy report file. Should be set individually when scanning multiple images in the same pipeline
+ * @return Returns true if the scan was ok (no vulnerability found); returns false if any vulnerability was found
+ */
+ boolean scanDogu(
+ String doguDir = ".",
+ String severityLevel = TrivySeverityLevel.CRITICAL,
+ String strategy = TrivyScanStrategy.UNSTABLE,
+ // Avoid rate limits of default Trivy database source
+ String additionalFlags = "--db-repository public.ecr.aws/aquasecurity/trivy-db --java-db-repository public.ecr.aws/aquasecurity/trivy-java-db",
+ String trivyReportFile = "trivy/trivyReport.json"
+ ) {
+ String image = script.sh(script: "jq .Image ${doguDir}/dogu.json", returnStdout: true).trim()
+ String version = script.sh(script: "jq .Version ${doguDir}/dogu.json", returnStdout: true).trim()
+ return scanImage(image + ":" + version, severityLevel, strategy, additionalFlags, trivyReportFile)
+ }
+
+ /**
+ * Save the Trivy scan results as a file with a specific format
+ *
+ * @param format The format of the output file {@link TrivyScanFormat}.
+ * You may enter supported formats (sarif, cyclonedx, spdx, spdx-json, github, cosign-vuln, table or json)
+ * or your own template ("template --template @FILENAME").
+ * If you want to convert to a format that requires a list of packages, such as SBOM, you need to add
+ * the `--list-all-pkgs` flag to the {@link Trivy#scanImage} call, when outputting in JSON
+ * (See trivy docs).
+ * @param severity Severities of security issues to be added (taken from UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL)
+ * @param formattedTrivyReportFilename The file name your report files should get, with file extension. E.g. "ubuntu24report.html"
+ * @param trivyReportFile The "trivyReportFile" parameter you used in the "scanImage" function, if it was set
+ */
+ void saveFormattedTrivyReport(String format = TrivyScanFormat.HTML,
+ String severity = TrivySeverityLevel.ALL,
+ String formattedTrivyReportFilename = null,
+ String trivyReportFile = "trivy/trivyReport.json") {
+
+ // set default report filename depending on the chosen format
+ if (formattedTrivyReportFilename == null) {
+ formattedTrivyReportFilename = "formattedTrivyReport" + getFileExtension(format)
+ }
+
+ String formatString
+ switch (format) {
+ // TrivyScanFormat.JSON and TrivyScanFormat.TABLE are handled by the default case, too
+ case TrivyScanFormat.HTML:
+ formatString = "template --template \"@/contrib/html.tpl\""
+ break
+ default:
+ // You may enter supported formats (sarif, cyclonedx, spdx, spdx-json, github, cosign-vuln, table or json)
+ // or your own template ("template --template @FILENAME")
+ List trivyFormats = ['sarif', 'cyclonedx', 'spdx', 'spdx-json', 'github', 'cosign-vuln', 'table', 'json']
+ // Check if "format" is a custom template from a file
+ boolean isTemplateFormat = format ==~ /^template --template @\S+$/
+ // Check if "format" is one of the trivyFormats or a template
+ if (trivyFormats.any { (format == it) } || isTemplateFormat) {
+ formatString = format
+ break
+ } else {
+ script.error("This format did not match the supported formats: " + format)
+ return
+ }
+ }
+ // Validate severity input parameter to prevent injection of additional parameters
+ if (!severity.split(',').every { it.trim() in ["UNKNOWN", "LOW", "MEDIUM", "HIGH", "CRITICAL"] }) {
+ script.error("The severity levels provided ($severity) do not match the applicable levels (UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL).")
+ }
+
+ docker.image("${trivyImage}:${trivyVersion}")
+ .inside("-v ${script.env.WORKSPACE}/.trivy/.cache:/root/.cache/") {
+ script.sh(script: "trivy convert --format ${formatString} --severity ${severity} --output ${trivyDirectory}/${formattedTrivyReportFilename} ${trivyReportFile}")
+ }
+ script.archiveArtifacts artifacts: "${trivyDirectory}/${formattedTrivyReportFilename}.*", allowEmptyArchive: true
+ }
+
+ private static String getFileExtension(String format) {
+ return TrivyScanFormat.isStandardScanFormat(format) ? "." + format : ".txt"
+ }
+}
diff --git a/src/com/cloudogu/ces/cesbuildlib/TrivyScanException.groovy b/src/com/cloudogu/ces/cesbuildlib/TrivyScanException.groovy
new file mode 100644
index 00000000..94e6fece
--- /dev/null
+++ b/src/com/cloudogu/ces/cesbuildlib/TrivyScanException.groovy
@@ -0,0 +1,10 @@
+package com.cloudogu.ces.cesbuildlib
+
+/**
+ * This exception is thrown whenever a vulnerability was found.
+ */
+class TrivyScanException extends RuntimeException {
+ TrivyScanException(String error) {
+ super(error)
+ }
+}
diff --git a/src/com/cloudogu/ces/cesbuildlib/TrivyScanFormat.groovy b/src/com/cloudogu/ces/cesbuildlib/TrivyScanFormat.groovy
new file mode 100644
index 00000000..d91c184b
--- /dev/null
+++ b/src/com/cloudogu/ces/cesbuildlib/TrivyScanFormat.groovy
@@ -0,0 +1,25 @@
+package com.cloudogu.ces.cesbuildlib
+
+/**
+ * Defines the output format for the trivy report.
+ */
+class TrivyScanFormat {
+ /**
+ * Output as HTML file.
+ */
+ static String HTML = "html"
+
+ /**
+ * Output as JSON file.
+ */
+ static String JSON = "json"
+
+ /**
+ * Output as table.
+ */
+ static String TABLE = "table"
+
+ static boolean isStandardScanFormat(String format) {
+ return format == HTML || format == JSON || format == TABLE
+ }
+}
diff --git a/src/com/cloudogu/ces/cesbuildlib/TrivyScanStrategy.groovy b/src/com/cloudogu/ces/cesbuildlib/TrivyScanStrategy.groovy
new file mode 100644
index 00000000..f9db8951
--- /dev/null
+++ b/src/com/cloudogu/ces/cesbuildlib/TrivyScanStrategy.groovy
@@ -0,0 +1,18 @@
+package com.cloudogu.ces.cesbuildlib
+
+class TrivyScanStrategy {
+ /**
+ * Strategy: Fail if any vulnerability was found.
+ */
+ static String FAIL = "fail"
+
+ /**
+ * Strategy: Make build unstable if any vulnerability was found.
+ */
+ static String UNSTABLE = "unstable"
+
+ /**
+ * Strategy: Ignore any found vulnerability.
+ */
+ static String IGNORE = "ignore"
+}
diff --git a/src/com/cloudogu/ces/cesbuildlib/TrivySeverityLevel.groovy b/src/com/cloudogu/ces/cesbuildlib/TrivySeverityLevel.groovy
new file mode 100644
index 00000000..8759bd0d
--- /dev/null
+++ b/src/com/cloudogu/ces/cesbuildlib/TrivySeverityLevel.groovy
@@ -0,0 +1,26 @@
+package com.cloudogu.ces.cesbuildlib
+
+/**
+ * Defines aggregated vulnerability levels
+ */
+class TrivySeverityLevel {
+ /**
+ * Only critical vulnerabilities.
+ */
+ static String CRITICAL = "CRITICAL"
+
+ /**
+ * High or critical vulnerabilities.
+ */
+ static String HIGH_AND_ABOVE = "CRITICAL,HIGH"
+
+ /**
+ * Medium or higher vulnerabilities.
+ */
+ static String MEDIUM_AND_ABOVE = "CRITICAL,HIGH,MEDIUM"
+
+ /**
+ * All vulnerabilities.
+ */
+ static String ALL = "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL"
+}
diff --git a/test/com/cloudogu/ces/cesbuildlib/GradleTest.groovy b/test/com/cloudogu/ces/cesbuildlib/GradleTest.groovy
index 8f7173eb..6aff2339 100644
--- a/test/com/cloudogu/ces/cesbuildlib/GradleTest.groovy
+++ b/test/com/cloudogu/ces/cesbuildlib/GradleTest.groovy
@@ -4,7 +4,7 @@ package com.cloudogu.ces.cesbuildlib
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
-import static org.junit.Assert.assertEquals
+import static org.junit.jupiter.api.Assertions.assertEquals
class GradleTest {
def scriptMock = new ScriptMock()
diff --git a/test/com/cloudogu/ces/cesbuildlib/MavenInDockerBaseTest.groovy b/test/com/cloudogu/ces/cesbuildlib/MavenInDockerBaseTest.groovy
index a0741513..337c340e 100644
--- a/test/com/cloudogu/ces/cesbuildlib/MavenInDockerBaseTest.groovy
+++ b/test/com/cloudogu/ces/cesbuildlib/MavenInDockerBaseTest.groovy
@@ -4,7 +4,7 @@ import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import static org.assertj.core.api.Assertions.assertThat
-import static org.junit.Assert.assertEquals
+import static org.junit.jupiter.api.Assertions.assertEquals
import static org.mockito.ArgumentMatchers.any
import static org.mockito.ArgumentMatchers.eq
import static org.mockito.Mockito.verify
diff --git a/test/com/cloudogu/ces/cesbuildlib/MavenTest.groovy b/test/com/cloudogu/ces/cesbuildlib/MavenTest.groovy
index 139eebc0..03d3d3dd 100644
--- a/test/com/cloudogu/ces/cesbuildlib/MavenTest.groovy
+++ b/test/com/cloudogu/ces/cesbuildlib/MavenTest.groovy
@@ -6,14 +6,14 @@ import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import static groovy.test.GroovyAssert.shouldFail
-import static org.junit.Assert.assertEquals
+import static org.junit.jupiter.api.Assertions.assertEquals
class MavenTest {
private static final String EOL = System.getProperty("line.separator")
static final EXPECTED_PWD = "/home/jenkins/workspaces/NAME"
def expectedDeploymentGoalWithStaging =
- 'org.sonatype.plugins:nexus-staging-maven-plugin:deploy -Dmaven.deploy.skip=true ' +
+ 'org.sonatype.plugins:nexus-staging-maven-plugin:deploy -Dmaven.deploy.skip=true ' +
'-DserverId=expectedId -DnexusUrl=https://expected.url -DautoReleaseAfterClose=true '
def scriptMock = new ScriptMock()
@@ -36,23 +36,23 @@ class MavenTest {
def result = mvn "test"
assertEquals("test", result)
}
-
+
@Test
void testWithoutSettingsXml() throws Exception {
def result = mvn "test"
- assert !(mvn.createCommandLineArgs ('dont care')).contains('settings.xml')
+ assert !(mvn.createCommandLineArgs('dont care')).contains('settings.xml')
assertEquals("test", result)
}
@Test
void testCallWithMirrors() throws Exception {
- mvn.useMirrors([name: 'n1', mirrorOf: 'm1', url: 'u1'],
- [name: 'n2', mirrorOf: 'm2', url: 'u2'])
+ mvn.useMirrors([name: 'n1', mirrorOf: 'm1', url: 'u1'],
+ [name: 'n2', mirrorOf: 'm2', url: 'u2'])
+
+ assert (mvn.createCommandLineArgs('dont care')).contains('settings.xml')
- assert (mvn.createCommandLineArgs ('dont care')).contains('settings.xml')
-
def result = mvn "test"
-
+
assertEquals("test", result)
assert scriptMock.writeFileParams.size() == 1
@@ -84,7 +84,7 @@ class MavenTest {
@Test
void testCallWithMultipleCredentials() throws Exception {
mvn.useRepositoryCredentials([id: 'number0', credentialsId: 'creds0'],
- [id: 'number1', credentialsId: 'creds1'])
+ [id: 'number1', credentialsId: 'creds1'])
def result = mvn "test"
assertEquals("test", result)
@@ -102,16 +102,14 @@ class MavenTest {
@Test
void testGetVersion() {
Maven mvn = new MavenForTest()
- assertEquals("Unexpected version returned",
- "org.apache.maven.plugins:maven-help-plugin:3.2.0:evaluate -Dexpression=project.version -q -DforceStdout", mvn.getVersion())
+ assertEquals("org.apache.maven.plugins:maven-help-plugin:3.2.0:evaluate -Dexpression=project.version -q -DforceStdout", mvn.getVersion(), "Unexpected version returned")
}
-
+
@Test
void testGetVersionWithCredentials() {
mvn.useRepositoryCredentials([id: 'number0', credentialsId: 'creds0'])
- assertEquals("Unexpected version returned",
- "org.apache.maven.plugins:maven-help-plugin:3.2.0:evaluate -Dexpression=project.version -q -DforceStdout", mvn.getVersion())
-
+ assertEquals("org.apache.maven.plugins:maven-help-plugin:3.2.0:evaluate -Dexpression=project.version -q -DforceStdout", mvn.getVersion(), "Unexpected version returned")
+
assert 'creds0' == scriptMock.actualUsernamePasswordArgs[0]['credentialsId']
assert "NEXUS_REPO_CREDENTIALS_PASSWORD_0" == scriptMock.actualUsernamePasswordArgs[0]['passwordVariable']
assert "NEXUS_REPO_CREDENTIALS_USERNAME_0" == scriptMock.actualUsernamePasswordArgs[0]['usernameVariable']
@@ -120,29 +118,25 @@ class MavenTest {
@Test
void testGetArtifactId() {
Maven mvn = new MavenForTest()
- assertEquals("Unexpected artifact returned",
- "org.apache.maven.plugins:maven-help-plugin:3.2.0:evaluate -Dexpression=project.artifactId -q -DforceStdout", mvn.getArtifactId())
+ assertEquals("org.apache.maven.plugins:maven-help-plugin:3.2.0:evaluate -Dexpression=project.artifactId -q -DforceStdout", mvn.getArtifactId(), "Unexpected artifact returned")
}
@Test
void testGetGroupId() {
Maven mvn = new MavenForTest()
- assertEquals("Unexpected group returned",
- "org.apache.maven.plugins:maven-help-plugin:3.2.0:evaluate -Dexpression=project.groupId -q -DforceStdout", mvn.getGroupId())
+ assertEquals("org.apache.maven.plugins:maven-help-plugin:3.2.0:evaluate -Dexpression=project.groupId -q -DforceStdout", mvn.getGroupId(), "Unexpected group returned")
}
@Test
void testGetName() {
Maven mvn = new MavenForTest()
- assertEquals("Unexpected name returned",
- "org.apache.maven.plugins:maven-help-plugin:3.2.0:evaluate -Dexpression=project.name -q -DforceStdout", mvn.getName())
+ assertEquals("org.apache.maven.plugins:maven-help-plugin:3.2.0:evaluate -Dexpression=project.name -q -DforceStdout", mvn.getName(), "Unexpected name returned")
}
@Test
void testGetMavenProperty() {
Maven mvn = new MavenForTest()
- assertEquals("Unexpected name returned",
- "org.apache.maven.plugins:maven-help-plugin:3.2.0:evaluate -Dexpression=key -q -DforceStdout", mvn.getMavenProperty('key'))
+ assertEquals("org.apache.maven.plugins:maven-help-plugin:3.2.0:evaluate -Dexpression=key -q -DforceStdout", mvn.getMavenProperty('key'), "Unexpected name returned")
}
@Test
@@ -162,16 +156,14 @@ class MavenTest {
@Test
void testGetMavenPropertyWithMavenWrapper() {
Maven mvn = new MavenWrapperForTest()
- assertEquals("Unexpected property returned",
- "org.apache.maven.plugins:maven-help-plugin:3.2.0:evaluate -Dexpression=key -q -DforceStdout", mvn.getMavenProperty('key'))
+ assertEquals("org.apache.maven.plugins:maven-help-plugin:3.2.0:evaluate -Dexpression=key -q -DforceStdout", mvn.getMavenProperty('key'), "Unexpected property returned")
}
@Test
void testGetMavenPropertyWithMavenWrapperNotYetDownloaded() {
Maven mvn = new MavenWrapperForTest()
mvn.downloaded = false
- assertEquals("Unexpected property returned",
- "org.apache.maven.plugins:maven-help-plugin:3.2.0:evaluate -Dexpression=key -q -DforceStdout", mvn.getMavenProperty('key'))
+ assertEquals("org.apache.maven.plugins:maven-help-plugin:3.2.0:evaluate -Dexpression=key -q -DforceStdout", mvn.getMavenProperty('key'), "Unexpected property returned")
}
@Test
@@ -185,13 +177,13 @@ class MavenTest {
@Test
void testUseRepositoryCredentialsMissingRequiredFieldsId() {
assertMissingRepositoryParameter('id',
- { mvn.useRepositoryCredentials([url: 'url', credentialsId: 'creds']) } )
+ { mvn.useRepositoryCredentials([url: 'url', credentialsId: 'creds']) })
}
@Test
void testUseRepositoryCredentialsMissingRequiredFields() {
assertMissingRepositoryParameter('credentialsIdUsernameAndPassword',
- { mvn.useRepositoryCredentials([url: 'url', id: 'id']) } )
+ { mvn.useRepositoryCredentials([url: 'url', id: 'id']) })
}
@Test
@@ -206,16 +198,16 @@ class MavenTest {
def expectedAdditionalArgs = 'expectedAdditionalArgs'
def actualAdditionalArgs = 'expectedAdditionalArgs'
deployToNexusRepository(actualAdditionalArgs, 'site:deploy',
- [[id: 'expectedId', credentialsId: 'expectedCredentials'],
- [id: 'id', url: 'https://expected.url', credentialsId: 'creds', type: 'Nexus2']],
- { mvn.deploySiteToNexus(expectedAdditionalArgs) })
+ [[id: 'expectedId', credentialsId: 'expectedCredentials'],
+ [id: 'id', url: 'https://expected.url', credentialsId: 'creds', type: 'Nexus2']],
+ { mvn.deploySiteToNexus(expectedAdditionalArgs) })
}
@Test
void testDeployToNexusRepositoryWithMultipleUrls() {
def exception = shouldFail {
mvn.useRepositoryCredentials([id: 'id', credentialsId: 'creds', url: '1'],
- [id: '2', credentialsId: 'creds2', url: '2'])
+ [id: '2', credentialsId: 'creds2', url: '2'])
}
assert "Multiple repositories with URL passed. Maven CLI only allows for passing one alt deployment repo." == exception.getMessage()
@@ -240,7 +232,7 @@ class MavenTest {
def expectedAdditionalArgs = 'expectedAdditionalArgs'
def actualAdditionalArgs = 'expectedAdditionalArgs'
deployToNexusRepository(DeployGoal.NEXUS_STAGING, expectedAdditionalArgs, actualAdditionalArgs,
- expectedDeploymentGoalWithStaging, 'source:jar javadoc:jar package')
+ expectedDeploymentGoalWithStaging, 'source:jar javadoc:jar package')
}
@@ -263,8 +255,8 @@ class MavenTest {
def expectedAdditionalArgs = 'expectedAdditionalArgs'
def actualAdditionalArgs = 'expectedAdditionalArgs'
deployToNexusRepository(actualAdditionalArgs, 'site:deploy',
- [[id: 'expectedId', credentialsId: 'expectedCredentials', type: 'Nexus2']],
- { mvn.deploySiteToNexus(expectedAdditionalArgs) })
+ [[id: 'expectedId', credentialsId: 'expectedCredentials', type: 'Nexus2']],
+ { mvn.deploySiteToNexus(expectedAdditionalArgs) })
}
@Test
@@ -272,8 +264,8 @@ class MavenTest {
def expectedAdditionalArgs = 'expectedAdditionalArgs'
def actualAdditionalArgs = 'expectedAdditionalArgs'
deployToNexusRepository(actualAdditionalArgs, 'site:deploy',
- [[id: 'expectedId', credentialsId: 'expectedCredentials', type: 'Nexus2']],
- { mvn.deploySiteToNexus(expectedAdditionalArgs) })
+ [[id: 'expectedId', credentialsId: 'expectedCredentials', type: 'Nexus2']],
+ { mvn.deploySiteToNexus(expectedAdditionalArgs) })
}
void deployToNexusRepositoryWithSignature(DeployGoal goal, String expectedDeploymentGoal, String beforeAdditionalArgs = '') {
@@ -298,9 +290,9 @@ class MavenTest {
private deployToNexusRepository(DeployGoal goal, String expectedAdditionalArgs, String actualAdditionalArgs,
String expectedDeploymentGoal, String beforeAdditionalArgs = '') {
deployToNexusRepository(actualAdditionalArgs, expectedDeploymentGoal,
- [[id : 'expectedId', url: 'https://expected.url', credentialsId: 'expectedCredentials', type: 'Nexus2']],
- { mvn.deployToNexusRepository(goal, expectedAdditionalArgs)},
- beforeAdditionalArgs
+ [[id: 'expectedId', url: 'https://expected.url', credentialsId: 'expectedCredentials', type: 'Nexus2']],
+ { mvn.deployToNexusRepository(goal, expectedAdditionalArgs) },
+ beforeAdditionalArgs
)
}
@@ -314,13 +306,12 @@ class MavenTest {
}
String deploymentRepoId = deploymentRepo.id
- def expectedCredentials = deploymentRepo.credentialsId
def expectedUrl = deploymentRepo.url
mvn.useRepositoryCredentials(repos.toArray(new Map[0]))
methodUnderTest.call()
-
- assert (mvn.createCommandLineArgs ('dont care')).contains('settings.xml')
+
+ assert (mvn.createCommandLineArgs('dont care')).contains('settings.xml')
def repoIds = []
for (int i = 0; i < repos.size(); i++) {
@@ -349,13 +340,13 @@ class MavenTest {
private void assertSettingsXmlRepos(String... deploymentRepoIds) {
assert scriptMock.writeFileParams.size() == 1
def actualSettingsXml = scriptMock.writeFileParams.get(0)['text']
-
+
for (int i = 0; i < deploymentRepoIds.size(); i++) {
def deploymentRepoId = deploymentRepoIds[i]
def string = "${deploymentRepoId}" +
- "\${env.NEXUS_REPO_CREDENTIALS_USERNAME_${i}}" +
- "\${env.NEXUS_REPO_CREDENTIALS_PASSWORD_${i}}"
+ "\${env.NEXUS_REPO_CREDENTIALS_USERNAME_${i}}" +
+ "\${env.NEXUS_REPO_CREDENTIALS_PASSWORD_${i}}"
assert actualSettingsXml.contains(string)
}
}
diff --git a/test/com/cloudogu/ces/cesbuildlib/SCMManagerTest.groovy b/test/com/cloudogu/ces/cesbuildlib/SCMManagerTest.groovy
index eb4aef46..66da7253 100644
--- a/test/com/cloudogu/ces/cesbuildlib/SCMManagerTest.groovy
+++ b/test/com/cloudogu/ces/cesbuildlib/SCMManagerTest.groovy
@@ -8,7 +8,7 @@ import org.junit.jupiter.api.Test
import static org.assertj.core.api.Assertions.assertThat
import static org.mockito.ArgumentMatchers.any
import static org.mockito.Mockito.mock
-import static org.mockito.Mockito.when
+import static org.mockito.Mockito.when
class SCMManagerTest {
@@ -19,7 +19,7 @@ class SCMManagerTest {
HttpClient httpMock
def slurper = new JsonSlurper()
-
+
def jsonTwoPrs = JsonOutput.toJson([
_embedded: [
pullRequests: [
@@ -34,7 +34,7 @@ class SCMManagerTest {
]
]
])
-
+
@BeforeEach
void init() {
httpMock = mock(HttpClient.class)
@@ -52,7 +52,7 @@ class SCMManagerTest {
body : jsonTwoPrs.toString()
]
})
-
+
def prs = scmm.searchPullRequestIdByTitle(repo, "one")
assertThat(prs).isEqualTo('1')
}
diff --git a/test/com/cloudogu/ces/cesbuildlib/SonarCloudTest.groovy b/test/com/cloudogu/ces/cesbuildlib/SonarCloudTest.groovy
index e497fa75..b93c1ba6 100644
--- a/test/com/cloudogu/ces/cesbuildlib/SonarCloudTest.groovy
+++ b/test/com/cloudogu/ces/cesbuildlib/SonarCloudTest.groovy
@@ -4,7 +4,7 @@ import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import static groovy.test.GroovyAssert.shouldFail
-import static org.junit.Assert.assertEquals
+import static org.junit.jupiter.api.Assertions.assertEquals
class SonarCloudTest {
diff --git a/test/com/cloudogu/ces/cesbuildlib/TrivyExecutor.groovy b/test/com/cloudogu/ces/cesbuildlib/TrivyExecutor.groovy
new file mode 100644
index 00000000..45dc3bc3
--- /dev/null
+++ b/test/com/cloudogu/ces/cesbuildlib/TrivyExecutor.groovy
@@ -0,0 +1,101 @@
+package com.cloudogu.ces.cesbuildlib
+
+import org.apache.commons.compress.archivers.ArchiveEntry
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream
+import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream
+
+import java.nio.channels.Channels
+import java.nio.channels.FileChannel
+import java.nio.channels.ReadableByteChannel
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.util.logging.Logger
+
+class TrivyExecutor {
+
+ private static final Logger logger = Logger.getLogger(TrivyExecutor.class.getName())
+ private Path installDir
+
+ TrivyExecutor(Path installDir = Paths.get("trivyInstallation")) {
+ this.installDir = installDir
+ }
+
+ Process exec(String version, String argumentstring, Path workDir) {
+ Path trivyPath = installTrivy(version)
+ if (workDir.getParent() != null) {
+ Files.createDirectories(workDir.getParent())
+ }
+
+ List arguments = new ArrayList()
+ arguments.add(trivyPath.toAbsolutePath().toString())
+ arguments.addAll(argumentstring.split(" "))
+ logger.info("start trivy: ${arguments.join(" ")}")
+ return new ProcessBuilder(arguments)
+ .directory(workDir.toAbsolutePath().toFile())
+ .inheritIO()
+ .start()
+ }
+
+ /**
+ * downloads, extracts and installs trivy as an executable file.
+ * Trivy is not downloaded again if the given version is already present.
+ * Each trivy version is installed into its own subdirectory to distinguish them.
+ * @param version trivy version
+ * @return the path to the trivy executable
+ */
+ private Path installTrivy(String version) {
+ Path pathToExtractedArchive = installDir.resolve("v${version}")
+ Path pathToTrivyExecutable = pathToExtractedArchive.resolve("trivy")
+ if (!pathToExtractedArchive.toFile().exists()) {
+ installDir.toFile().mkdirs()
+ File archive = downloadTrivy(version, installDir)
+ untar(archive, pathToExtractedArchive)
+ logger.info("delete trivy download archive $pathToExtractedArchive")
+ if (!archive.delete()) {
+ throw new RuntimeException("cannot delete trivy download archive: $pathToExtractedArchive")
+ }
+
+ logger.fine("make $pathToTrivyExecutable an executable")
+ if (pathToTrivyExecutable.toFile().setExecutable(true)) {
+ return pathToTrivyExecutable
+ } else {
+ throw new RuntimeException("cannot make trivy executable: ${pathToTrivyExecutable}")
+ }
+ } else {
+ logger.info("trivy v${version} already installed")
+ }
+
+ return pathToTrivyExecutable
+ }
+
+ private static File downloadTrivy(String version, Path downloadDir) {
+ URL url = new URL("https://github.com/aquasecurity/trivy/releases/download/v${version}/trivy_${version}_Linux-64bit.tar.gz")
+ File archive = downloadDir.resolve("trivy.tar.gz").toFile()
+ archive.createNewFile()
+ logger.info("download trivy v${version} from $url to $archive")
+
+ ReadableByteChannel readableByteChannel = Channels.newChannel(url.openStream())
+ FileOutputStream fileOutputStream = new FileOutputStream(archive)
+ FileChannel fileChannel = fileOutputStream.getChannel()
+ fileChannel.transferFrom(readableByteChannel, 0, Long.MAX_VALUE)
+ return archive
+ }
+
+ private static void untar(File archive, Path destination) throws IOException {
+ BufferedInputStream inputStream = new BufferedInputStream(archive.newInputStream())
+ TarArchiveInputStream tar = new TarArchiveInputStream(new GzipCompressorInputStream(inputStream))
+ logger.info("untar $archive to $destination")
+ try {
+ ArchiveEntry entry
+ while ((entry = tar.getNextEntry()) != null) {
+ Path extractTo = entry.resolveIn(destination)
+ logger.info("untar: extract entry to ${extractTo}")
+ Files.createDirectories(extractTo.getParent())
+ Files.copy(tar, extractTo)
+ }
+ } finally {
+ inputStream.close()
+ }
+ }
+}
diff --git a/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy b/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy
new file mode 100644
index 00000000..2557766f
--- /dev/null
+++ b/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy
@@ -0,0 +1,235 @@
+package com.cloudogu.ces.cesbuildlib
+
+import org.junit.jupiter.api.Test
+import org.mockito.invocation.InvocationOnMock
+import org.mockito.stubbing.Answer
+import org.opentest4j.AssertionFailedError
+
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.util.concurrent.TimeUnit
+
+import static org.junit.jupiter.api.Assertions.*
+import static org.mockito.ArgumentMatchers.any
+import static org.mockito.ArgumentMatchers.matches
+import static org.mockito.Mockito.mock
+import static org.mockito.Mockito.when
+
+class TrivyTest {
+
+ String additionalFlags = "--db-repository public.ecr.aws/aquasecurity/trivy-db --java-db-repository public.ecr.aws/aquasecurity/trivy-java-db"
+ Path installDir = Paths.get("target/trivyInstalls")
+ Path workDir = Paths.get("")
+ TrivyExecutor trivyExec = new TrivyExecutor(installDir)
+ String trivyImage = "aquasec/trivy:" + Trivy.DEFAULT_TRIVY_VERSION
+
+
+ ScriptMock doTestScan(String imageName, String severityLevel, String strategy, int expectedStatusCode) {
+ File trivyReportFile = new File("trivy/trivyReport.json")
+ Path trivyDir = Paths.get(trivyReportFile.getParent())
+ String trivyArguments = "image --exit-code 10 --exit-on-eol 10 --format ${TrivyScanFormat.JSON} -o ${trivyReportFile} --severity ${severityLevel} ${additionalFlags} ${imageName}"
+ String expectedTrivyCommand = "trivy $trivyArguments"
+
+ def scriptMock = new ScriptMock()
+ scriptMock.env.WORKSPACE = "/test"
+ Docker dockerMock = mock(Docker.class)
+ Docker.Image imageMock = mock(Docker.Image.class)
+ when(dockerMock.image(trivyImage)).thenReturn(imageMock)
+ when(imageMock.mountJenkinsUser()).thenReturn(imageMock)
+ when(imageMock.mountDockerSocket()).thenReturn(imageMock)
+ when(imageMock.inside(matches("-v /test/.trivy/.cache:/root/.cache/"), any())).thenAnswer(new Answer() {
+ @Override
+ Integer answer(InvocationOnMock invocation) throws Throwable {
+ // mock "sh trivy" so that it returns the expected status code and check trivy arguments
+ Closure closure = invocation.getArgument(1)
+ scriptMock.expectedShRetValueForScript.put(expectedTrivyCommand, expectedStatusCode)
+ Integer statusCode = closure.call() as Integer
+ assertEquals(expectedStatusCode, statusCode)
+ assertEquals(expectedTrivyCommand, scriptMock.getActualShMapArgs().getLast())
+
+ // emulate trivy call with local trivy installation and check that it has the same behavior
+ Files.createDirectories(trivyDir)
+ Process process = trivyExec.exec(Trivy.DEFAULT_TRIVY_VERSION, trivyArguments, workDir)
+ if (process.waitFor(2, TimeUnit.MINUTES)) {
+ assertEquals(expectedStatusCode, process.exitValue())
+ } else {
+ process.destroyForcibly()
+ fail("terminate trivy due to timeout")
+ }
+
+ return expectedStatusCode
+ }
+ })
+ Trivy trivy = new Trivy(scriptMock, Trivy.DEFAULT_TRIVY_VERSION, Trivy.DEFAULT_TRIVY_IMAGE, dockerMock)
+
+ trivy.scanImage(imageName, severityLevel, strategy)
+
+ return scriptMock
+ }
+
+ @Test
+ void testScanImage_successfulTrivyExecution() {
+ // with hopes that this image will never have CVEs
+ String imageName = "hello-world"
+ String severityLevel = TrivySeverityLevel.CRITICAL
+
+ def scriptMock = doTestScan(imageName, severityLevel, TrivyScanStrategy.UNSTABLE, 0)
+
+ assertEquals(false, scriptMock.getUnstable())
+ }
+
+ @Test
+ void testScanImage_unstableBecauseOfCVEs() {
+ // with hopes that this image will always have CVEs
+ String imageName = "alpine:3.18.7"
+ String severityLevel = TrivySeverityLevel.ALL
+
+ def scriptMock = doTestScan(imageName, severityLevel, TrivyScanStrategy.UNSTABLE, 10)
+
+ assertEquals(true, scriptMock.getUnstable())
+ }
+
+ @Test
+ void testScanImage_failBecauseOfCVEs() {
+ // with hopes that this image will always have CVEs
+ String imageName = "alpine:3.18.7"
+ String severityLevel = TrivySeverityLevel.ALL
+
+ def gotException = false
+ try {
+ doTestScan(imageName, severityLevel, TrivyScanStrategy.FAIL, 10)
+ } catch (AssertionFailedError e) {
+ // exception could also be a junit assertion exception. This means a previous assertion failed
+ throw e
+ } catch (Exception e) {
+ assertTrue(e.getMessage().contains("Trivy has found vulnerabilities in image"), "exception is: ${e.getMessage()}")
+ gotException = true
+ }
+ assertTrue(gotException)
+ }
+
+ @Test
+ void testScanImage_unsuccessfulTrivyExecution() {
+ String imageName = "inval!d:::///1.1...1.1."
+ String severityLevel = TrivySeverityLevel.ALL
+
+ def gotException = false
+ try {
+ doTestScan(imageName, severityLevel, TrivyScanStrategy.FAIL, 1)
+ } catch (AssertionFailedError e) {
+ // exception could also be a junit assertion exception. This means a previous assertion failed
+ throw e
+ } catch (Exception e) {
+ assertTrue(e.getMessage().contains("Error during trivy scan; exit code: 1"), "exception is: ${e.getMessage()}")
+ gotException = true
+ }
+ assertTrue(gotException)
+ }
+
+ @Test
+ void testSaveFormattedTrivyReport_HtmlAllSeverities() {
+ Trivy trivy = mockTrivy(
+ "template --template \"@/contrib/html.tpl\"",
+ "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL",
+ "trivy/formattedTrivyReport.html")
+ trivy.saveFormattedTrivyReport()
+ }
+
+ @Test
+ void testSaveFormattedTrivyReport_JsonCriticalSeverity() {
+ Trivy trivy = mockTrivy(
+ "json",
+ "CRITICAL",
+ "trivy/formattedTrivyReport.json")
+ trivy.saveFormattedTrivyReport(TrivyScanFormat.JSON, TrivySeverityLevel.CRITICAL)
+ }
+
+ @Test
+ void testSaveFormattedTrivyReport_TableHighAndUpSeverity() {
+ Trivy trivy = mockTrivy(
+ "table",
+ "CRITICAL,HIGH",
+ "trivy/formattedTrivyReport.table")
+ trivy.saveFormattedTrivyReport(TrivyScanFormat.TABLE, TrivySeverityLevel.HIGH_AND_ABOVE)
+ }
+
+ @Test
+ void testSaveFormattedTrivyReport_MediumAndUpSeverity() {
+ Trivy trivy = mockTrivy(
+ "sarif",
+ "CRITICAL,HIGH,MEDIUM",
+ "trivy/formattedTrivyReport.txt")
+ trivy.saveFormattedTrivyReport("sarif", TrivySeverityLevel.MEDIUM_AND_ABOVE)
+ }
+
+ @Test
+ void testSaveFormattedTrivyReport_CustomFilename() {
+ Trivy trivy = mockTrivy(
+ "json",
+ "CRITICAL,HIGH,MEDIUM",
+ "trivy/myOutput.custom")
+ trivy.saveFormattedTrivyReport(TrivyScanFormat.JSON, TrivySeverityLevel.MEDIUM_AND_ABOVE, "myOutput.custom")
+ }
+
+ @Test
+ void testSaveFormattedTrivyReport_UnsupportedFormat() {
+ def scriptMock = new ScriptMock()
+ scriptMock.env.WORKSPACE = "/test"
+ Trivy trivy = new Trivy(scriptMock, Trivy.DEFAULT_TRIVY_VERSION, Trivy.DEFAULT_TRIVY_IMAGE, mock(Docker.class))
+ def gotException = false
+ try {
+ trivy.saveFormattedTrivyReport("UnsupportedFormat", TrivySeverityLevel.MEDIUM_AND_ABOVE)
+ } catch (AssertionFailedError e) {
+ // exception could also be a junit assertion exception. This means a previous assertion failed
+ throw e
+ } catch (Exception e) {
+ assertTrue(e.getMessage().contains("This format did not match the supported formats"), "exception is: ${e.getMessage()}")
+ gotException = true
+ }
+ assertTrue(gotException)
+ }
+
+ @Test
+ void testSaveFormattedTrivyReport_UnsupportedSeverity() {
+ def scriptMock = new ScriptMock()
+ scriptMock.env.WORKSPACE = "/test"
+ Trivy trivy = new Trivy(scriptMock, Trivy.DEFAULT_TRIVY_VERSION, Trivy.DEFAULT_TRIVY_IMAGE, mock(Docker.class))
+ def gotException = false
+ try {
+ trivy.saveFormattedTrivyReport(TrivyScanFormat.JSON, "UnsupportedSeverity")
+ } catch (AssertionFailedError e) {
+ // exception could also be a junit assertion exception. This means a previous assertion failed
+ throw e
+ } catch (Exception e) {
+ assertTrue(e.getMessage().contains("The severity levels provided (UnsupportedSeverity) do not match the " +
+ "applicable levels (UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL)."), "exception is: ${e.getMessage()}")
+ gotException = true
+ }
+ assertTrue(gotException)
+ }
+
+ Trivy mockTrivy(String expectedFormat, String expectedSeverity, String expectedOutput) {
+ String trivyArguments = "convert --format ${expectedFormat} --severity ${expectedSeverity} --output ${expectedOutput} trivy/trivyReport.json"
+ String expectedTrivyCommand = "trivy $trivyArguments"
+
+ def scriptMock = new ScriptMock()
+ scriptMock.env.WORKSPACE = "/test"
+ Docker dockerMock = mock(Docker.class)
+ Docker.Image imageMock = mock(Docker.Image.class)
+ when(dockerMock.image(trivyImage)).thenReturn(imageMock)
+ when(imageMock.inside(matches("-v /test/.trivy/.cache:/root/.cache/"), any())).thenAnswer(new Answer() {
+ @Override
+ Integer answer(InvocationOnMock invocation) throws Throwable {
+ // mock "sh trivy" so that it returns the expected status code and check trivy arguments
+ Closure closure = invocation.getArgument(1)
+ scriptMock.expectedShRetValueForScript.put(expectedTrivyCommand, 0)
+ closure.call()
+ assertEquals(expectedTrivyCommand, scriptMock.getActualShMapArgs().getLast())
+ return 0
+ }
+ })
+ Trivy trivy = new Trivy(scriptMock, Trivy.DEFAULT_TRIVY_VERSION, Trivy.DEFAULT_TRIVY_IMAGE, dockerMock)
+ return trivy
+ }
+}
diff --git a/vars/findVulnerabilitiesWithTrivy.groovy b/vars/findVulnerabilitiesWithTrivy.groovy
index 60241fb9..7d7813dd 100644
--- a/vars/findVulnerabilitiesWithTrivy.groovy
+++ b/vars/findVulnerabilitiesWithTrivy.groovy
@@ -10,8 +10,8 @@ ArrayList call (Map args) {
if(args.containsKey('allowList'))
error "Arg allowList is deprecated, please use .trivyignore file"
def imageName = args.imageName
- def trivyVersion = args.trivyVersion ? args.trivyVersion : '0.55.2'
- def severityFlag = args.severity ? "--severity=${args.severity.join(',')}" : ''
+ def trivyVersion = args.trivyVersion ? args.trivyVersion : '0.57.1'
+ def severityFlag = args.severity ? "${args.severity.join(',')}" : ''
def additionalFlags = args.additionalFlags ? args.additionalFlags : ''
println(severityFlag)
@@ -27,7 +27,8 @@ ArrayList call (Map args) {
ArrayList getVulnerabilities(String trivyVersion, String severityFlag, String additionalFlags,String imageName) {
// this runs trivy and creates an output file with found vulnerabilities
- runTrivyInDocker(trivyVersion, severityFlag, additionalFlags, imageName)
+ Trivy trivy = new Trivy(this, trivyVersion)
+ trivy.scanImage(imageName, severityFlag, TrivyScanStrategy.UNSTABLE, additionalFlags, "${env.WORKSPACE}/.trivy/trivyOutput.json")
def trivyOutput = readJSON file: "${env.WORKSPACE}/.trivy/trivyOutput.json"
@@ -42,21 +43,6 @@ ArrayList getVulnerabilities(String trivyVersion, String severityFlag, String ad
}
-
-
-
-def runTrivyInDocker(String trivyVersion, severityFlag, additionalFlags, imageName) {
- new Docker(this).image("aquasec/trivy:${trivyVersion}")
- .mountJenkinsUser()
- .mountDockerSocket()
- .inside("-v ${env.WORKSPACE}/.trivy/.cache:/root/.cache/") {
-
- sh "trivy image -f json -o .trivy/trivyOutput.json ${severityFlag} ${additionalFlags} ${imageName}"
- }
-}
-
-
-
static boolean validateArgs(Map args) {
return !(args == null || args.imageName == null || args.imageName == '')
}