From c2897fb9495a724e05362527adfe485a39f1a75f Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Mon, 18 Nov 2024 13:47:08 +0100 Subject: [PATCH 01/55] Add Trivy implementation structure; #136 --- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 44 +++++++++++++++++++ .../ces/cesbuildlib/TrivyScanFormat.groovy | 21 +++++++++ .../ces/cesbuildlib/TrivyScanLevel.groovy | 26 +++++++++++ .../ces/cesbuildlib/TrivyScanStrategy.groovy | 18 ++++++++ 4 files changed, 109 insertions(+) create mode 100644 src/com/cloudogu/ces/cesbuildlib/Trivy.groovy create mode 100644 src/com/cloudogu/ces/cesbuildlib/TrivyScanFormat.groovy create mode 100644 src/com/cloudogu/ces/cesbuildlib/TrivyScanLevel.groovy create mode 100644 src/com/cloudogu/ces/cesbuildlib/TrivyScanStrategy.groovy diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy new file mode 100644 index 00000000..752c4bfe --- /dev/null +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -0,0 +1,44 @@ +package com.cloudogu.ces.cesbuildlib + +class Trivy implements Serializable { + def script + String trivyReportFilename + + Trivy(script, String trivyReportFilename = "${env.WORKSPACE}/.trivy/trivyReport.json") { + this.script = script + this.trivyReportFilename = trivyReportFilename + } + + /** + * 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() + * - Evaluate via exit codes: 0 = no vulnerability; 1 = vulnerabilities found; other = function call failed + * + * @param imageName The image name; may include version tag + * @param trivyVersion The version of Trivy used for scanning + * @param additionalFlags Additional Trivy command flags + * @param scanLevel The vulnerability level to scan. Can be a member of TrivyScanLevel 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) + * // TODO: A strategy could be implemented by the user via the exit codes of this function. Should we remove the strategy parameter? + * @return Returns 0 if the scan was ok (no vulnerability found); returns 1 if any vulnerability was found + */ + int scanImage(String imageName, String trivyVersion = "0.57.0", String additionalFlags, String scanLevel = TrivyScanLevel.CRITICAL, String strategy = TrivyScanStrategy.FAIL) { + // TODO: Run trivy scan inside Docker container, e.g. via Jenkins' Docker.image() function + // See runTrivyInDocker function: https://github.com/cloudogu/ces-build-lib/blob/c48273409f8f506e31872fe2857650bbfc76a222/vars/findVulnerabilitiesWithTrivy.groovy#L48 + // TODO: Write result to trivyReportFile in json format (--format json), which can be converted in the saveFormattedTrivyReport function + // TODO: Include .trivyignore file, if existent. Do not fail if .trivyignore file does not exist. + } + + /** + * Save the Trivy scan results as a file with a specific format + * + * @param format The format of the output file (@see TrivyScanFormat) + */ + void saveFormattedTrivyReport(String format = TrivyScanFormat.HTML) { + // TODO: DO NOT scan again! Take the trivyReportFile and convert its content + // See https://aquasecurity.github.io/trivy/v0.52/docs/references/configuration/cli/trivy_convert/ + } + +} diff --git a/src/com/cloudogu/ces/cesbuildlib/TrivyScanFormat.groovy b/src/com/cloudogu/ces/cesbuildlib/TrivyScanFormat.groovy new file mode 100644 index 00000000..45164c8a --- /dev/null +++ b/src/com/cloudogu/ces/cesbuildlib/TrivyScanFormat.groovy @@ -0,0 +1,21 @@ +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 plain text file. + */ + static String PLAIN = "plain" +} diff --git a/src/com/cloudogu/ces/cesbuildlib/TrivyScanLevel.groovy b/src/com/cloudogu/ces/cesbuildlib/TrivyScanLevel.groovy new file mode 100644 index 00000000..1c7c10c5 --- /dev/null +++ b/src/com/cloudogu/ces/cesbuildlib/TrivyScanLevel.groovy @@ -0,0 +1,26 @@ +package com.cloudogu.ces.cesbuildlib + +/** + * Defines aggregated vulnerability levels + */ +class TrivyScanLevel { + /** + * Only critical vulnerabilities. + */ + static String CRITICAL = "CRITICAL" + + /** + * High or critical vulnerabilities. + */ + static String HIGH = "CRITICAL,HIGH" + + /** + * Medium or higher vulnerabilities. + */ + static String MEDIUM = "CRITICAL,HIGH,MEDIUM" + + /** + * All vunlerabilities. + */ + static String ALL = "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL" +} 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" +} From 251cdf1c73e3971a38a275052058069a716ee02f Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Mon, 18 Nov 2024 14:56:41 +0100 Subject: [PATCH 02/55] Add invalid image name test; #136 --- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 12 +++++++----- .../cloudogu/ces/cesbuildlib/TrivyTest.groovy | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index 752c4bfe..4c944008 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -1,10 +1,10 @@ package com.cloudogu.ces.cesbuildlib class Trivy implements Serializable { - def script - String trivyReportFilename + private script + private String trivyReportFilename - Trivy(script, String trivyReportFilename = "${env.WORKSPACE}/.trivy/trivyReport.json") { + Trivy(script, String trivyReportFilename = "${script.env.WORKSPACE}/.trivy/trivyReport.json") { this.script = script this.trivyReportFilename = trivyReportFilename } @@ -16,7 +16,7 @@ class Trivy implements Serializable { * - This function will generate a JSON formatted report file which can be converted to other formats via saveFormattedTrivyReport() * - Evaluate via exit codes: 0 = no vulnerability; 1 = vulnerabilities found; other = function call failed * - * @param imageName The image name; may include version tag + * @param imageName The name of the image to be scanned; may include a version tag * @param trivyVersion The version of Trivy used for scanning * @param additionalFlags Additional Trivy command flags * @param scanLevel The vulnerability level to scan. Can be a member of TrivyScanLevel or a custom String (e.g. 'CRITICAL,LOW') @@ -24,11 +24,13 @@ class Trivy implements Serializable { * // TODO: A strategy could be implemented by the user via the exit codes of this function. Should we remove the strategy parameter? * @return Returns 0 if the scan was ok (no vulnerability found); returns 1 if any vulnerability was found */ - int scanImage(String imageName, String trivyVersion = "0.57.0", String additionalFlags, String scanLevel = TrivyScanLevel.CRITICAL, String strategy = TrivyScanStrategy.FAIL) { + int scanImage(String imageName, String trivyVersion = "0.57.0", String additionalFlags = "", String scanLevel = TrivyScanLevel.CRITICAL, String strategy = TrivyScanStrategy.FAIL) { + int exitCode = 255 // TODO: Run trivy scan inside Docker container, e.g. via Jenkins' Docker.image() function // See runTrivyInDocker function: https://github.com/cloudogu/ces-build-lib/blob/c48273409f8f506e31872fe2857650bbfc76a222/vars/findVulnerabilitiesWithTrivy.groovy#L48 // TODO: Write result to trivyReportFile in json format (--format json), which can be converted in the saveFormattedTrivyReport function // TODO: Include .trivyignore file, if existent. Do not fail if .trivyignore file does not exist. + return exitCode } /** diff --git a/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy b/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy new file mode 100644 index 00000000..9ce57f65 --- /dev/null +++ b/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy @@ -0,0 +1,18 @@ +package com.cloudogu.ces.cesbuildlib + +class TrivyTest extends GroovyTestCase { + + void testScanImage_invalidImageName() { + def scriptMock = new ScriptMock() + scriptMock.env.WORKSPACE = "." + Trivy trivy = new Trivy(scriptMock) + + int result = trivy.scanImage("invalid///:::1.1.!!.1.1") + + assertNotSame(0, result) + assertNotSame(1, result) + } + + void testSaveFormattedTrivyReport() { + } +} From ec14c1267aab0cf8ececaa9a059bae841522282f Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Mon, 18 Nov 2024 15:03:54 +0100 Subject: [PATCH 03/55] Fail pipeline on failing unit tests --- Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 19107473..98ae560b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -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 +} From 8cc5d403c3044c217e99c9df7dfd3e8ef765560b Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Tue, 19 Nov 2024 15:59:55 +0100 Subject: [PATCH 04/55] Implement scanImage method; #136 --- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 37 ++++++++++++----- .../ces/cesbuildlib/TrivyScanException.groovy | 10 +++++ ...Level.groovy => TrivySeverityLevel.groovy} | 4 +- .../cloudogu/ces/cesbuildlib/TrivyTest.groovy | 41 ++++++++++++++++--- 4 files changed, 74 insertions(+), 18 deletions(-) create mode 100644 src/com/cloudogu/ces/cesbuildlib/TrivyScanException.groovy rename src/com/cloudogu/ces/cesbuildlib/{TrivyScanLevel.groovy => TrivySeverityLevel.groovy} (89%) diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index 4c944008..7f710d69 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -1,12 +1,15 @@ package com.cloudogu.ces.cesbuildlib class Trivy implements Serializable { + private static final String DEFAULT_TRIVY_VERSION = "0.57.0" private script private String trivyReportFilename + private Docker docker - Trivy(script, String trivyReportFilename = "${script.env.WORKSPACE}/.trivy/trivyReport.json") { + Trivy(script, String trivyReportFilename = "${script.env.WORKSPACE}/.trivy/trivyReport.json", Docker docker = new Docker(script)) { this.script = script this.trivyReportFilename = trivyReportFilename + this.docker = docker } /** @@ -19,18 +22,32 @@ class Trivy implements Serializable { * @param imageName The name of the image to be scanned; may include a version tag * @param trivyVersion The version of Trivy used for scanning * @param additionalFlags Additional Trivy command flags - * @param scanLevel The vulnerability level to scan. Can be a member of TrivyScanLevel or a custom String (e.g. 'CRITICAL,LOW') + * @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) - * // TODO: A strategy could be implemented by the user via the exit codes of this function. Should we remove the strategy parameter? - * @return Returns 0 if the scan was ok (no vulnerability found); returns 1 if any vulnerability was found + * @return Returns true if the scan was ok (no vulnerability found); returns false if any vulnerability was found */ - int scanImage(String imageName, String trivyVersion = "0.57.0", String additionalFlags = "", String scanLevel = TrivyScanLevel.CRITICAL, String strategy = TrivyScanStrategy.FAIL) { - int exitCode = 255 - // TODO: Run trivy scan inside Docker container, e.g. via Jenkins' Docker.image() function - // See runTrivyInDocker function: https://github.com/cloudogu/ces-build-lib/blob/c48273409f8f506e31872fe2857650bbfc76a222/vars/findVulnerabilitiesWithTrivy.groovy#L48 - // TODO: Write result to trivyReportFile in json format (--format json), which can be converted in the saveFormattedTrivyReport function + boolean scanImage(String imageName, String trivyVersion = DEFAULT_TRIVY_VERSION, String additionalFlags = "", String severityLevel = TrivySeverityLevel.CRITICAL, String strategy = TrivyScanStrategy.FAIL) { + int exitCode + docker.image("aquasec/trivy:${trivyVersion}") + .mountJenkinsUser() + .mountDockerSocket() + .inside("-v ${script.env.WORKSPACE}/.trivy/.cache:/root/.cache/") { + // Write result to $trivyReportFilename in json format (--format json), which can be converted in the saveFormattedTrivyReport function + // Exit with exit code 1 if vulnerabilities are found + exitCode = sh(script: "trivy image --exit-code 1 --format json -o ${trivyReportFilename} --severity ${severityLevel} ${additionalFlags} ${imageName}", returnStatus: true) + } + switch (exitCode) { + case 0: + // Everything all right, no vulnerabilities + return true + case 1: + // Found vulnerabilities + // TODO: Set build status according to strategy + return false + default: + throw new TrivyScanException("Error during trivy scan; exit code: " + exitCode) + } // TODO: Include .trivyignore file, if existent. Do not fail if .trivyignore file does not exist. - return exitCode } /** 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/TrivyScanLevel.groovy b/src/com/cloudogu/ces/cesbuildlib/TrivySeverityLevel.groovy similarity index 89% rename from src/com/cloudogu/ces/cesbuildlib/TrivyScanLevel.groovy rename to src/com/cloudogu/ces/cesbuildlib/TrivySeverityLevel.groovy index 1c7c10c5..3d780621 100644 --- a/src/com/cloudogu/ces/cesbuildlib/TrivyScanLevel.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/TrivySeverityLevel.groovy @@ -3,7 +3,7 @@ package com.cloudogu.ces.cesbuildlib /** * Defines aggregated vulnerability levels */ -class TrivyScanLevel { +class TrivySeverityLevel { /** * Only critical vulnerabilities. */ @@ -20,7 +20,7 @@ class TrivyScanLevel { static String MEDIUM = "CRITICAL,HIGH,MEDIUM" /** - * All vunlerabilities. + * All vulnerabilities. */ static String ALL = "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL" } diff --git a/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy b/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy index 9ce57f65..cea65d85 100644 --- a/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy +++ b/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy @@ -1,18 +1,47 @@ package com.cloudogu.ces.cesbuildlib +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 extends GroovyTestCase { - void testScanImage_invalidImageName() { + void testScanImage_successfulTrivyExecution() { + def scriptMock = new ScriptMock() + scriptMock.env.WORKSPACE = "/test" + Docker dockerMock = mock(Docker.class) + Docker.Image imageMock = mock(Docker.Image.class) + when(dockerMock.image("aquasec/trivy:"+Trivy.DEFAULT_TRIVY_VERSION)).thenReturn(imageMock) + when(imageMock.mountJenkinsUser()).thenReturn(imageMock) + when(imageMock.mountDockerSocket()).thenReturn(imageMock) + when(imageMock.inside(matches("-v /test/.trivy/.cache:/root/.cache/"), any())).thenReturn(null) + Trivy trivy = new Trivy(scriptMock, "/.trivy/trivyReport.json", dockerMock) + + trivy.scanImage("nginx") + // TODO: check that the build is not marked as unstable + } + + void testScanImage_unsuccessfulTrivyExecution() { def scriptMock = new ScriptMock() - scriptMock.env.WORKSPACE = "." - Trivy trivy = new Trivy(scriptMock) + scriptMock.env.WORKSPACE = "/test" + Docker dockerMock = mock(Docker.class) + Docker.Image imageMock = mock(Docker.Image.class) + when(dockerMock.image("aquasec/trivy:"+Trivy.DEFAULT_TRIVY_VERSION)).thenReturn(imageMock) + when(imageMock.mountJenkinsUser()).thenReturn(imageMock) + when(imageMock.mountDockerSocket()).thenReturn(imageMock) + when(imageMock.inside(matches("-v /test/.trivy/.cache:/root/.cache/"), any())).thenThrow(new RuntimeException("Trivy scan had errors: ")) + Trivy trivy = new Trivy(scriptMock, "/.trivy/trivyReport.json", dockerMock) - int result = trivy.scanImage("invalid///:::1.1.!!.1.1") + def exception = shouldFail { + trivy.scanImage("inval!d:::///1.1...1.1.") + } + assert exception.contains("Trivy scan had errors: ") - assertNotSame(0, result) - assertNotSame(1, result) + // TODO: check that the build is marked as failed } void testSaveFormattedTrivyReport() { + notYetImplemented() } } From cc9b3e6bcfacb54b603350c29b0d65806fbd9068 Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Wed, 20 Nov 2024 07:38:35 +0100 Subject: [PATCH 05/55] Handle null references; #136 --- src/com/cloudogu/ces/cesbuildlib/Docker.groovy | 2 +- src/com/cloudogu/ces/cesbuildlib/Sh.groovy | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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() } } From 5a97314e75b3f782e4ea5846e1d62295462e5ed9 Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Wed, 20 Nov 2024 13:40:51 +0100 Subject: [PATCH 06/55] Move parameters to sensible locations; #136 --- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 16 +++++++++------- .../cloudogu/ces/cesbuildlib/TrivyTest.groovy | 4 ++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index 7f710d69..50a3b08e 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -1,15 +1,17 @@ package com.cloudogu.ces.cesbuildlib class Trivy implements Serializable { - private static final String DEFAULT_TRIVY_VERSION = "0.57.0" + private static final String DEFAULT_TRIVY_VERSION = "0.57.1" private script - private String trivyReportFilename private Docker docker + private String trivyVersion + private String trivyDirectory = ".trivy" + private String trivyReportFilenameWithoutExtension = "/"+trivyDirectory+"/trivyReport" - Trivy(script, String trivyReportFilename = "${script.env.WORKSPACE}/.trivy/trivyReport.json", Docker docker = new Docker(script)) { + Trivy(script, Docker docker = new Docker(script), String trivyVersion = DEFAULT_TRIVY_VERSION) { this.script = script - this.trivyReportFilename = trivyReportFilename this.docker = docker + this.trivyVersion = trivyVersion } /** @@ -26,7 +28,7 @@ class Trivy implements Serializable { * @param strategy The strategy to follow after the scan. Should the build become unstable or failed? Or Should any vulnerability be ignored? (@see TrivyScanStrategy) * @return Returns true if the scan was ok (no vulnerability found); returns false if any vulnerability was found */ - boolean scanImage(String imageName, String trivyVersion = DEFAULT_TRIVY_VERSION, String additionalFlags = "", String severityLevel = TrivySeverityLevel.CRITICAL, String strategy = TrivyScanStrategy.FAIL) { + boolean scanImage(String imageName, String trivyReportFilename = "${script.env.WORKSPACE}/.trivy/trivyReport.json", String additionalFlags = "", String severityLevel = TrivySeverityLevel.CRITICAL, String strategy = TrivyScanStrategy.FAIL) { int exitCode docker.image("aquasec/trivy:${trivyVersion}") .mountJenkinsUser() @@ -34,7 +36,8 @@ class Trivy implements Serializable { .inside("-v ${script.env.WORKSPACE}/.trivy/.cache:/root/.cache/") { // Write result to $trivyReportFilename in json format (--format json), which can be converted in the saveFormattedTrivyReport function // Exit with exit code 1 if vulnerabilities are found - exitCode = sh(script: "trivy image --exit-code 1 --format json -o ${trivyReportFilename} --severity ${severityLevel} ${additionalFlags} ${imageName}", returnStatus: true) + script.sh("mkdir -p " + trivyDirectory) + exitCode = script.sh(script: "trivy image --exit-code 1 --format " + TrivyScanFormat.JSON + " -o ${trivyReportFilename} --severity ${severityLevel} ${additionalFlags} ${imageName}", returnStatus: true) } switch (exitCode) { case 0: @@ -59,5 +62,4 @@ class Trivy implements Serializable { // TODO: DO NOT scan again! Take the trivyReportFile and convert its content // See https://aquasecurity.github.io/trivy/v0.52/docs/references/configuration/cli/trivy_convert/ } - } diff --git a/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy b/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy index cea65d85..591c370b 100644 --- a/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy +++ b/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy @@ -16,7 +16,7 @@ class TrivyTest extends GroovyTestCase { when(imageMock.mountJenkinsUser()).thenReturn(imageMock) when(imageMock.mountDockerSocket()).thenReturn(imageMock) when(imageMock.inside(matches("-v /test/.trivy/.cache:/root/.cache/"), any())).thenReturn(null) - Trivy trivy = new Trivy(scriptMock, "/.trivy/trivyReport.json", dockerMock) + Trivy trivy = new Trivy(scriptMock, dockerMock) trivy.scanImage("nginx") // TODO: check that the build is not marked as unstable @@ -31,7 +31,7 @@ class TrivyTest extends GroovyTestCase { when(imageMock.mountJenkinsUser()).thenReturn(imageMock) when(imageMock.mountDockerSocket()).thenReturn(imageMock) when(imageMock.inside(matches("-v /test/.trivy/.cache:/root/.cache/"), any())).thenThrow(new RuntimeException("Trivy scan had errors: ")) - Trivy trivy = new Trivy(scriptMock, "/.trivy/trivyReport.json", dockerMock) + Trivy trivy = new Trivy(scriptMock, dockerMock) def exception = shouldFail { trivy.scanImage("inval!d:::///1.1...1.1.") From 034130417b6e2cc7f9a59983e0667de1659d26e5 Mon Sep 17 00:00:00 2001 From: Alexander Dammeier Date: Wed, 20 Nov 2024 15:08:38 +0100 Subject: [PATCH 07/55] try to fix pipeline error --- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index 50a3b08e..ff83b306 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -1,12 +1,11 @@ package com.cloudogu.ces.cesbuildlib class Trivy implements Serializable { - private static final String DEFAULT_TRIVY_VERSION = "0.57.1" + static final String DEFAULT_TRIVY_VERSION = "0.57.1" private script private Docker docker private String trivyVersion private String trivyDirectory = ".trivy" - private String trivyReportFilenameWithoutExtension = "/"+trivyDirectory+"/trivyReport" Trivy(script, Docker docker = new Docker(script), String trivyVersion = DEFAULT_TRIVY_VERSION) { this.script = script @@ -28,16 +27,22 @@ class Trivy implements Serializable { * @param strategy The strategy to follow after the scan. Should the build become unstable or failed? Or Should any vulnerability be ignored? (@see TrivyScanStrategy) * @return Returns true if the scan was ok (no vulnerability found); returns false if any vulnerability was found */ - boolean scanImage(String imageName, String trivyReportFilename = "${script.env.WORKSPACE}/.trivy/trivyReport.json", String additionalFlags = "", String severityLevel = TrivySeverityLevel.CRITICAL, String strategy = TrivyScanStrategy.FAIL) { + boolean scanImage( + String imageName, + String trivyReportFilename = "${this.script.env.WORKSPACE}/.trivy/trivyReport.json", + String additionalFlags = "", + String severityLevel = TrivySeverityLevel.CRITICAL, + String strategy = TrivyScanStrategy.FAIL + ) { int exitCode - docker.image("aquasec/trivy:${trivyVersion}") + this.docker.image("aquasec/trivy:${trivyVersion}") .mountJenkinsUser() .mountDockerSocket() .inside("-v ${script.env.WORKSPACE}/.trivy/.cache:/root/.cache/") { // Write result to $trivyReportFilename in json format (--format json), which can be converted in the saveFormattedTrivyReport function // Exit with exit code 1 if vulnerabilities are found script.sh("mkdir -p " + trivyDirectory) - exitCode = script.sh(script: "trivy image --exit-code 1 --format " + TrivyScanFormat.JSON + " -o ${trivyReportFilename} --severity ${severityLevel} ${additionalFlags} ${imageName}", returnStatus: true) + exitCode = script.sh(script: "trivy image --exit-code 1 exit-on-eol 1 --format ${TrivyScanFormat.JSON} -o ${trivyReportFilename} --severity ${severityLevel} ${additionalFlags} ${imageName}", returnStatus: true) } switch (exitCode) { case 0: @@ -53,11 +58,11 @@ class Trivy implements Serializable { // TODO: Include .trivyignore file, if existent. Do not fail if .trivyignore file does not exist. } - /** - * Save the Trivy scan results as a file with a specific format - * - * @param format The format of the output file (@see TrivyScanFormat) - */ + /** + * Save the Trivy scan results as a file with a specific format + * + * @param format The format of the output file (@see TrivyScanFormat) + */ void saveFormattedTrivyReport(String format = TrivyScanFormat.HTML) { // TODO: DO NOT scan again! Take the trivyReportFile and convert its content // See https://aquasecurity.github.io/trivy/v0.52/docs/references/configuration/cli/trivy_convert/ From 6d6dbb7d76968045dbc2ac1d84213f6738c35f5e Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Thu, 21 Nov 2024 08:58:29 +0100 Subject: [PATCH 08/55] Fix parameter call --- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index ff83b306..6f9c7549 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -42,7 +42,7 @@ class Trivy implements Serializable { // Write result to $trivyReportFilename in json format (--format json), which can be converted in the saveFormattedTrivyReport function // Exit with exit code 1 if vulnerabilities are found script.sh("mkdir -p " + trivyDirectory) - exitCode = script.sh(script: "trivy image --exit-code 1 exit-on-eol 1 --format ${TrivyScanFormat.JSON} -o ${trivyReportFilename} --severity ${severityLevel} ${additionalFlags} ${imageName}", returnStatus: true) + exitCode = script.sh(script: "trivy image --exit-code 1 --exit-on-eol 1 --format ${TrivyScanFormat.JSON} -o ${trivyReportFilename} --severity ${severityLevel} ${additionalFlags} ${imageName}", returnStatus: true) } switch (exitCode) { case 0: From d5b41b1ca97b1201ca58e954c1017bc688a8e2a9 Mon Sep 17 00:00:00 2001 From: Alexander Dammeier Date: Thu, 21 Nov 2024 09:18:20 +0100 Subject: [PATCH 09/55] #136 fix groovy compile error with constant --- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index 6f9c7549..714e67df 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -4,10 +4,10 @@ class Trivy implements Serializable { static final String DEFAULT_TRIVY_VERSION = "0.57.1" private script private Docker docker - private String trivyVersion + private String trivyVersion = DEFAULT_TRIVY_VERSION private String trivyDirectory = ".trivy" - Trivy(script, Docker docker = new Docker(script), String trivyVersion = DEFAULT_TRIVY_VERSION) { + Trivy(script, Docker docker = new Docker(script), String trivyVersion = this.trivyVersion) { this.script = script this.docker = docker this.trivyVersion = trivyVersion @@ -35,7 +35,7 @@ class Trivy implements Serializable { String strategy = TrivyScanStrategy.FAIL ) { int exitCode - this.docker.image("aquasec/trivy:${trivyVersion}") + docker.image("aquasec/trivy:${trivyVersion}") .mountJenkinsUser() .mountDockerSocket() .inside("-v ${script.env.WORKSPACE}/.trivy/.cache:/root/.cache/") { From 77ad25b24eee33a357c3eec4189fc687a6feccff Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Thu, 21 Nov 2024 09:24:47 +0100 Subject: [PATCH 10/55] Fix NullPointerException --- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index 714e67df..5c548164 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -4,10 +4,10 @@ class Trivy implements Serializable { static final String DEFAULT_TRIVY_VERSION = "0.57.1" private script private Docker docker - private String trivyVersion = DEFAULT_TRIVY_VERSION + private String trivyVersion private String trivyDirectory = ".trivy" - Trivy(script, Docker docker = new Docker(script), String trivyVersion = this.trivyVersion) { + Trivy(script, Docker docker = new Docker(script), String trivyVersion = "0.57.1") { this.script = script this.docker = docker this.trivyVersion = trivyVersion From c30624e49ef6299d786c5598f1334d64e8642546 Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Thu, 21 Nov 2024 11:47:51 +0100 Subject: [PATCH 11/55] Use custom exit code on found vulnerabilities; 136 As trivy exits with exit code 1 when it encountered an error, we will use exit code 10 if trivy scanned successful and found vulnerabilities. Also don't use RuntimeException, which is blocked by Jenkins. --- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index 5c548164..53df2e30 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -42,18 +42,18 @@ class Trivy implements Serializable { // Write result to $trivyReportFilename in json format (--format json), which can be converted in the saveFormattedTrivyReport function // Exit with exit code 1 if vulnerabilities are found script.sh("mkdir -p " + trivyDirectory) - exitCode = script.sh(script: "trivy image --exit-code 1 --exit-on-eol 1 --format ${TrivyScanFormat.JSON} -o ${trivyReportFilename} --severity ${severityLevel} ${additionalFlags} ${imageName}", returnStatus: true) + exitCode = script.sh(script: "trivy image --exit-code 10 --exit-on-eol 10 --format ${TrivyScanFormat.JSON} -o ${trivyReportFilename} --severity ${severityLevel} ${additionalFlags} ${imageName}", returnStatus: true) } switch (exitCode) { case 0: // Everything all right, no vulnerabilities return true - case 1: + case 10: // Found vulnerabilities // TODO: Set build status according to strategy return false default: - throw new TrivyScanException("Error during trivy scan; exit code: " + exitCode) + script.error("Error during trivy scan; exit code: " + exitCode) } // TODO: Include .trivyignore file, if existent. Do not fail if .trivyignore file does not exist. } From a65c6e2c22fbb36a373d591a00ccd3b952fdf112 Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Thu, 21 Nov 2024 13:26:09 +0100 Subject: [PATCH 12/55] Implement report conversion function; #136 --- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 25 ++++++++++++++++--- .../ces/cesbuildlib/TrivyScanFormat.groovy | 4 +-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index 53df2e30..737e27ff 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -6,6 +6,7 @@ class Trivy implements Serializable { private Docker docker private String trivyVersion private String trivyDirectory = ".trivy" + private String trivyReportFilenameWithoutExtension = trivyDirectory+"/trivyReport" Trivy(script, Docker docker = new Docker(script), String trivyVersion = "0.57.1") { this.script = script @@ -63,8 +64,26 @@ class Trivy implements Serializable { * * @param format The format of the output file (@see TrivyScanFormat) */ - void saveFormattedTrivyReport(String format = TrivyScanFormat.HTML) { - // TODO: DO NOT scan again! Take the trivyReportFile and convert its content - // See https://aquasecurity.github.io/trivy/v0.52/docs/references/configuration/cli/trivy_convert/ + void saveFormattedTrivyReport(String format = TrivyScanFormat.HTML, String trivyReportFilename = "${script.env.WORKSPACE}/.trivy/trivyReport.json") { + String formatExtension + switch (format) { + case TrivyScanFormat.HTML: + formatExtension = "html" + // TODO: html is no standard convert format. Use a template! + case TrivyScanFormat.JSON: + // Result file is already in JSON format + return + case TrivyScanFormat.TABLE: + formatExtension = "table" + default: + // TODO: Do nothing? Throw exception? idk + break + } + docker.image("aquasec/trivy:${trivyVersion}") + .inside("-v ${script.env.WORKSPACE}/.trivy/.cache:/root/.cache/") { + script.sh(script: "trivy convert --format ${format} --output ${trivyReportFilenameWithoutExtension}.${formatExtension} ${trivyReportFilename}") + } + + script.archiveArtifacts artifacts: "${trivyReportFilenameWithoutExtension}.${format}", allowEmptyArchive: true } } diff --git a/src/com/cloudogu/ces/cesbuildlib/TrivyScanFormat.groovy b/src/com/cloudogu/ces/cesbuildlib/TrivyScanFormat.groovy index 45164c8a..c2131ebf 100644 --- a/src/com/cloudogu/ces/cesbuildlib/TrivyScanFormat.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/TrivyScanFormat.groovy @@ -15,7 +15,7 @@ class TrivyScanFormat { static String JSON = "json" /** - * Output as plain text file. + * Output as table. */ - static String PLAIN = "plain" + static String TABLE = "table" } From a28ad47ab183734d17467596757387397fdc7267 Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Thu, 21 Nov 2024 14:10:23 +0100 Subject: [PATCH 13/55] Save all trivy report files; #136 --- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index 737e27ff..f6f64764 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -5,7 +5,7 @@ class Trivy implements Serializable { private script private Docker docker private String trivyVersion - private String trivyDirectory = ".trivy" + private String trivyDirectory = "trivy" private String trivyReportFilenameWithoutExtension = trivyDirectory+"/trivyReport" Trivy(script, Docker docker = new Docker(script), String trivyVersion = "0.57.1") { @@ -30,7 +30,7 @@ class Trivy implements Serializable { */ boolean scanImage( String imageName, - String trivyReportFilename = "${this.script.env.WORKSPACE}/.trivy/trivyReport.json", + String trivyReportFilename = "${this.script.env.WORKSPACE}/trivy/trivyReport.json", String additionalFlags = "", String severityLevel = TrivySeverityLevel.CRITICAL, String strategy = TrivyScanStrategy.FAIL @@ -64,7 +64,7 @@ class Trivy implements Serializable { * * @param format The format of the output file (@see TrivyScanFormat) */ - void saveFormattedTrivyReport(String format = TrivyScanFormat.HTML, String trivyReportFilename = "${script.env.WORKSPACE}/.trivy/trivyReport.json") { + void saveFormattedTrivyReport(String format = TrivyScanFormat.HTML, String trivyReportFilename = "${script.env.WORKSPACE}/trivy/trivyReport.json") { String formatExtension switch (format) { case TrivyScanFormat.HTML: @@ -84,6 +84,6 @@ class Trivy implements Serializable { script.sh(script: "trivy convert --format ${format} --output ${trivyReportFilenameWithoutExtension}.${formatExtension} ${trivyReportFilename}") } - script.archiveArtifacts artifacts: "${trivyReportFilenameWithoutExtension}.${format}", allowEmptyArchive: true + script.archiveArtifacts artifacts: "${trivyReportFilenameWithoutExtension}.*", allowEmptyArchive: true } } From 096a0aa0c59f08e1ea05de6deeabbe196ab617ef Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Thu, 21 Nov 2024 15:03:52 +0100 Subject: [PATCH 14/55] Add HTML format conversion; #136 --- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index f6f64764..ecabd391 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -65,25 +65,28 @@ class Trivy implements Serializable { * @param format The format of the output file (@see TrivyScanFormat) */ void saveFormattedTrivyReport(String format = TrivyScanFormat.HTML, String trivyReportFilename = "${script.env.WORKSPACE}/trivy/trivyReport.json") { - String formatExtension + String fileExtension + String formatString switch (format) { case TrivyScanFormat.HTML: - formatExtension = "html" - // TODO: html is no standard convert format. Use a template! + formatString = "template --template \"@/contrib/html.tpl\"" + fileExtension = "html" + break case TrivyScanFormat.JSON: // Result file is already in JSON format return case TrivyScanFormat.TABLE: - formatExtension = "table" + formatString = "table" + fileExtension = "txt" + break default: // TODO: Do nothing? Throw exception? idk break } docker.image("aquasec/trivy:${trivyVersion}") .inside("-v ${script.env.WORKSPACE}/.trivy/.cache:/root/.cache/") { - script.sh(script: "trivy convert --format ${format} --output ${trivyReportFilenameWithoutExtension}.${formatExtension} ${trivyReportFilename}") + script.sh(script: "trivy convert --format ${formatString} --output ${trivyReportFilenameWithoutExtension}.${fileExtension} ${trivyReportFilename}") } - script.archiveArtifacts artifacts: "${trivyReportFilenameWithoutExtension}.*", allowEmptyArchive: true } } From c933342735f11184e5126e7af9fcb155f069a432 Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Mon, 25 Nov 2024 07:57:02 +0100 Subject: [PATCH 15/55] Fail on unsupported scan format; #136 --- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index ecabd391..9583d5f0 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -80,8 +80,8 @@ class Trivy implements Serializable { fileExtension = "txt" break default: - // TODO: Do nothing? Throw exception? idk - break + script.error("This format did not match the supported formats: " + format) + return } docker.image("aquasec/trivy:${trivyVersion}") .inside("-v ${script.env.WORKSPACE}/.trivy/.cache:/root/.cache/") { From 7777d4d572cacc5bd2fbb4d285acc60ad94ccaa3 Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Mon, 25 Nov 2024 09:27:21 +0100 Subject: [PATCH 16/55] Trivy ignore file is working To exclude CVEs from trivy scan, add a .trivyignore file to the workspace. The workspace is mounted to the docker container and therefore the .trivyignore file is integrated into the scan process, if it exists --- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 1 - 1 file changed, 1 deletion(-) diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index 9583d5f0..3976b498 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -56,7 +56,6 @@ class Trivy implements Serializable { default: script.error("Error during trivy scan; exit code: " + exitCode) } - // TODO: Include .trivyignore file, if existent. Do not fail if .trivyignore file does not exist. } /** From 358b7e21707b765777d622c5afce933edf01b986 Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Mon, 25 Nov 2024 10:34:32 +0100 Subject: [PATCH 17/55] Enable report file name setting; #136 --- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index 3976b498..09a6d093 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -6,7 +6,6 @@ class Trivy implements Serializable { private Docker docker private String trivyVersion private String trivyDirectory = "trivy" - private String trivyReportFilenameWithoutExtension = trivyDirectory+"/trivyReport" Trivy(script, Docker docker = new Docker(script), String trivyVersion = "0.57.1") { this.script = script @@ -30,20 +29,20 @@ class Trivy implements Serializable { */ boolean scanImage( String imageName, - String trivyReportFilename = "${this.script.env.WORKSPACE}/trivy/trivyReport.json", String additionalFlags = "", String severityLevel = TrivySeverityLevel.CRITICAL, - String strategy = TrivyScanStrategy.FAIL + String strategy = TrivyScanStrategy.FAIL, + String trivyReportFile = "trivy/trivyReport.json" ) { int exitCode docker.image("aquasec/trivy:${trivyVersion}") .mountJenkinsUser() .mountDockerSocket() .inside("-v ${script.env.WORKSPACE}/.trivy/.cache:/root/.cache/") { - // Write result to $trivyReportFilename in json format (--format json), which can be converted in the saveFormattedTrivyReport function + // Write result to $trivyReportFile in json format (--format json), which can be converted in the saveFormattedTrivyReport function // Exit with exit code 1 if vulnerabilities are found script.sh("mkdir -p " + trivyDirectory) - exitCode = script.sh(script: "trivy image --exit-code 10 --exit-on-eol 10 --format ${TrivyScanFormat.JSON} -o ${trivyReportFilename} --severity ${severityLevel} ${additionalFlags} ${imageName}", returnStatus: true) + exitCode = 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: @@ -62,18 +61,22 @@ class Trivy implements Serializable { * Save the Trivy scan results as a file with a specific format * * @param format The format of the output file (@see TrivyScanFormat) + * @param formattedTrivyReportFilename The file name your report files should get, without file extension. E.g. "ubuntu24report" + * @param trivyReportFile The "trivyReportFile" parameter you used in the "scanImage" function, if it was set */ - void saveFormattedTrivyReport(String format = TrivyScanFormat.HTML, String trivyReportFilename = "${script.env.WORKSPACE}/trivy/trivyReport.json") { + void saveFormattedTrivyReport(String format = TrivyScanFormat.HTML, String formattedTrivyReportFilename = "trivyReport", String trivyReportFile = "trivy/trivyReport.json") { String fileExtension String formatString + String trivyDirectory = "trivy/" switch (format) { case TrivyScanFormat.HTML: formatString = "template --template \"@/contrib/html.tpl\"" fileExtension = "html" break case TrivyScanFormat.JSON: - // Result file is already in JSON format - return + formatString = "json" + fileExtension = "json" + break case TrivyScanFormat.TABLE: formatString = "table" fileExtension = "txt" @@ -84,8 +87,8 @@ class Trivy implements Serializable { } docker.image("aquasec/trivy:${trivyVersion}") .inside("-v ${script.env.WORKSPACE}/.trivy/.cache:/root/.cache/") { - script.sh(script: "trivy convert --format ${formatString} --output ${trivyReportFilenameWithoutExtension}.${fileExtension} ${trivyReportFilename}") + script.sh(script: "trivy convert --format ${formatString} --output ${trivyDirectory}${formattedTrivyReportFilename}.${fileExtension} ${trivyReportFile}") } - script.archiveArtifacts artifacts: "${trivyReportFilenameWithoutExtension}.*", allowEmptyArchive: true + script.archiveArtifacts artifacts: "${trivyDirectory}${formattedTrivyReportFilename}.*", allowEmptyArchive: true } } From 190fb089ef270caaa7739fa05179861e52a96af4 Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Mon, 25 Nov 2024 11:13:20 +0100 Subject: [PATCH 18/55] Avoid rate limits of default Trivy database source; #136 --- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index 09a6d093..9efea695 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -29,9 +29,10 @@ class Trivy implements Serializable { */ boolean scanImage( String imageName, - String additionalFlags = "", String severityLevel = TrivySeverityLevel.CRITICAL, String strategy = TrivyScanStrategy.FAIL, + // 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" ) { int exitCode From 7bd456209eb5e9448f64b8ba9d06a70cd93352e5 Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Mon, 25 Nov 2024 11:31:14 +0100 Subject: [PATCH 19/55] Set build status according to strategy; #136 --- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index 9efea695..1af67d8a 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -30,7 +30,7 @@ class Trivy implements Serializable { boolean scanImage( String imageName, String severityLevel = TrivySeverityLevel.CRITICAL, - String strategy = TrivyScanStrategy.FAIL, + 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" @@ -51,7 +51,19 @@ class Trivy implements Serializable { return true case 10: // Found vulnerabilities - // TODO: Set build status according to strategy + // 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) From 4183ba217419ac61447c64c701245e01ae11dde1 Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Mon, 25 Nov 2024 16:14:42 +0100 Subject: [PATCH 20/55] Document Trivy functionality; #136 --- README.md | 152 ++++++++++++++---- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 4 +- 2 files changed, 124 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 052010eb..1e930968 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,124 @@ shellCheck(fileList) // fileList="a.sh b.sh" execute shellcheck on a custom list See [shellCheck](vars/shellCheck.groovy) +# Trivy + +Scan 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 explicit Docker registry +Docker docker = new Docker(this) +docker.withRegistry("https://my.registry.invalid", myRegistryCredentialsID) +Trivy trivy = new Trivy(this, "0.57.1", 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. + +### 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, "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) +``` + +## 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 +1412,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 +1451,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/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index 1af67d8a..f4951714 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -7,10 +7,10 @@ class Trivy implements Serializable { private String trivyVersion private String trivyDirectory = "trivy" - Trivy(script, Docker docker = new Docker(script), String trivyVersion = "0.57.1") { + Trivy(script, String trivyVersion = "0.57.1", Docker docker = new Docker(script)) { this.script = script - this.docker = docker this.trivyVersion = trivyVersion + this.docker = docker } /** From 5bf7fdf30c8b376e10eca780420dab5a7b40ed97 Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Tue, 26 Nov 2024 10:52:49 +0100 Subject: [PATCH 21/55] Adapt tests to new Trivy constructor; #136 --- test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy b/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy index 591c370b..8c07a459 100644 --- a/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy +++ b/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy @@ -16,10 +16,11 @@ class TrivyTest extends GroovyTestCase { when(imageMock.mountJenkinsUser()).thenReturn(imageMock) when(imageMock.mountDockerSocket()).thenReturn(imageMock) when(imageMock.inside(matches("-v /test/.trivy/.cache:/root/.cache/"), any())).thenReturn(null) - Trivy trivy = new Trivy(scriptMock, dockerMock) + Trivy trivy = new Trivy(scriptMock, "0.57.1", dockerMock) trivy.scanImage("nginx") - // TODO: check that the build is not marked as unstable + + assertEquals(false, scriptMock.getUnstable()) } void testScanImage_unsuccessfulTrivyExecution() { @@ -31,14 +32,12 @@ class TrivyTest extends GroovyTestCase { when(imageMock.mountJenkinsUser()).thenReturn(imageMock) when(imageMock.mountDockerSocket()).thenReturn(imageMock) when(imageMock.inside(matches("-v /test/.trivy/.cache:/root/.cache/"), any())).thenThrow(new RuntimeException("Trivy scan had errors: ")) - Trivy trivy = new Trivy(scriptMock, dockerMock) + Trivy trivy = new Trivy(scriptMock, "0.57.1", dockerMock) def exception = shouldFail { trivy.scanImage("inval!d:::///1.1...1.1.") } assert exception.contains("Trivy scan had errors: ") - - // TODO: check that the build is marked as failed } void testSaveFormattedTrivyReport() { From a32591fff6de6225d06a0c2d82fe57a189eafeaf Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Wed, 27 Nov 2024 13:41:28 +0100 Subject: [PATCH 22/55] Add method to scan Dogu images; #136 --- README.md | 18 +++++++++++ src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 31 +++++++++++++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1e930968..5fe3be1c 100644 --- a/README.md +++ b/README.md @@ -1329,6 +1329,24 @@ trivy.saveFormattedTrivyReport(TrivyScanFormat.JSON) trivy.saveFormattedTrivyReport(TrivyScanFormat.HTML) ``` +## Scan Dogu image with Trivy + +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 +Trivy trivy = new Trivy(this) +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 diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index f4951714..7214fe64 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -21,10 +21,10 @@ class Trivy implements Serializable { * - Evaluate via exit codes: 0 = no vulnerability; 1 = vulnerabilities found; other = function call failed * * @param imageName The name of the image to be scanned; may include a version tag - * @param trivyVersion The version of Trivy used for scanning - * @param additionalFlags Additional Trivy command flags * @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( @@ -70,6 +70,33 @@ class Trivy implements Serializable { } } + /** + * 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() + * - Evaluate via exit codes: 0 = no vulnerability; 1 = vulnerabilities found; other = function call failed + * + * @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) + String version = script.sh(script: "jq .Version ${doguDir}/dogu.json", returnStdout: true) + return scanImage(image+":"+version, severityLevel, strategy, additionalFlags, trivyReportFile) + } + /** * Save the Trivy scan results as a file with a specific format * From 122c87dce296d39565d116d1732b1a68e4f6f549 Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Wed, 27 Nov 2024 14:28:16 +0100 Subject: [PATCH 23/55] Remove newlines from ssh output; #136 --- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index 7214fe64..fbbc8991 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -92,8 +92,8 @@ class Trivy implements Serializable { 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) - String version = script.sh(script: "jq .Version ${doguDir}/dogu.json", returnStdout: true) + 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) } From 326db5c63f46a80d9302b3bf77381d6c194fa573 Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Wed, 27 Nov 2024 08:53:47 +0100 Subject: [PATCH 24/55] #136 implement trivy integration test --- .gitignore | 1 + README.md | 2 +- pom.xml | 15 +++ src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 5 +- .../ces/cesbuildlib/TrivyExecutor.groovy | 101 ++++++++++++++++++ .../cloudogu/ces/cesbuildlib/TrivyTest.groovy | 57 +++++++++- 6 files changed, 173 insertions(+), 8 deletions(-) create mode 100644 test/com/cloudogu/ces/cesbuildlib/TrivyExecutor.groovy 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/README.md b/README.md index 5fe3be1c..d5097d8d 100644 --- a/README.md +++ b/README.md @@ -1243,7 +1243,7 @@ See [shellCheck](vars/shellCheck.groovy) # Trivy -Scan images for vulnerabilities with Trivy. +Scan container images for vulnerabilities with Trivy. ## Create a Trivy object diff --git a/pom.xml b/pom.xml index 8438be3c..87ddd37d 100644 --- a/pom.xml +++ b/pom.xml @@ -25,6 +25,8 @@ UTF-8 0.8.5 + 11 + 11 @@ -68,6 +70,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/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index fbbc8991..2faa82ec 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -35,15 +35,14 @@ class Trivy implements Serializable { 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" ) { - int exitCode - docker.image("aquasec/trivy:${trivyVersion}") + Integer exitCode = docker.image("aquasec/trivy:${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 1 if vulnerabilities are found script.sh("mkdir -p " + trivyDirectory) - exitCode = script.sh(script: "trivy image --exit-code 10 --exit-on-eol 10 --format ${TrivyScanFormat.JSON} -o ${trivyReportFile} --severity ${severityLevel} ${additionalFlags} ${imageName}", returnStatus: true) + 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: 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 index 8c07a459..ab4f95c6 100644 --- a/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy +++ b/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy @@ -1,5 +1,14 @@ package com.cloudogu.ces.cesbuildlib + +import org.mockito.invocation.InvocationOnMock +import org.mockito.stubbing.Answer + +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.util.concurrent.TimeUnit + import static org.mockito.ArgumentMatchers.any import static org.mockito.ArgumentMatchers.matches import static org.mockito.Mockito.mock @@ -7,18 +16,58 @@ import static org.mockito.Mockito.when class TrivyTest extends GroovyTestCase { + + void testScanImage_successfulTrivyExecution() { + + Path installDir = Paths.get("target/trivyInstalls") + Path workDir = Paths.get("") + TrivyExecutor trivyExec = new TrivyExecutor(installDir) + + + // with hopes that this image will never have CVEs + String imageName = "hello-world" + String severityLevel = TrivySeverityLevel.CRITICAL + String strategy = TrivyScanStrategy.UNSTABLE + String additionalFlags = "--db-repository public.ecr.aws/aquasecurity/trivy-db --java-db-repository public.ecr.aws/aquasecurity/trivy-java-db" + 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" + + String trivyImage = "aquasec/trivy:" + Trivy.DEFAULT_TRIVY_VERSION def scriptMock = new ScriptMock() scriptMock.env.WORKSPACE = "/test" Docker dockerMock = mock(Docker.class) Docker.Image imageMock = mock(Docker.Image.class) - when(dockerMock.image("aquasec/trivy:"+Trivy.DEFAULT_TRIVY_VERSION)).thenReturn(imageMock) + 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())).thenReturn(null) - Trivy trivy = new Trivy(scriptMock, "0.57.1", dockerMock) + when(imageMock.inside(matches("-v /test/.trivy/.cache:/root/.cache/"), any())).thenAnswer(new Answer() { + @Override + Integer answer(InvocationOnMock invocation) throws Throwable { + Integer expectedStatusCode = 0 + Closure closure = invocation.getArgument(1) + scriptMock.expectedShRetValueForScript.put(expectedTrivyCommand, expectedStatusCode) + Integer statusCode = closure.call() as Integer + assertEquals(expectedStatusCode, statusCode) + assertEquals(expectedTrivyCommand, scriptMock.getActualShMapArgs().getLast()) + + 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 statusCode + } + }) + Trivy trivy = new Trivy(scriptMock, Trivy.DEFAULT_TRIVY_VERSION, dockerMock) - trivy.scanImage("nginx") + trivy.scanImage(imageName) assertEquals(false, scriptMock.getUnstable()) } From 91bf4ca299f8265cc46478172c8f05f4580eff96 Mon Sep 17 00:00:00 2001 From: Alexander Dammeier Date: Wed, 27 Nov 2024 15:25:00 +0100 Subject: [PATCH 25/55] #136 add comments to integration test --- test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy b/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy index ab4f95c6..fe32a3a3 100644 --- a/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy +++ b/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy @@ -46,6 +46,7 @@ class TrivyTest extends GroovyTestCase { 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 Integer expectedStatusCode = 0 Closure closure = invocation.getArgument(1) scriptMock.expectedShRetValueForScript.put(expectedTrivyCommand, expectedStatusCode) @@ -53,6 +54,7 @@ class TrivyTest extends GroovyTestCase { 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)) { From e6736d9512cd2dfd088516c0260aecbb6098e8a8 Mon Sep 17 00:00:00 2001 From: Alexander Dammeier Date: Wed, 27 Nov 2024 15:39:30 +0100 Subject: [PATCH 26/55] #136 use javac compiler as default also update compiler dependencies --- pom.xml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 44e3eb36..b8c64f85 100644 --- a/pom.xml +++ b/pom.xml @@ -98,20 +98,17 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 - - groovy-eclipse-compiler - + 3.13.0 org.codehaus.groovy groovy-eclipse-compiler - 3.3.0-01 + 3.7.0 org.codehaus.groovy groovy-eclipse-batch - 2.5.6-01 + 3.0.8-01 From f651b320ce40ff58b459adec595162cf3b27a7fa Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Thu, 28 Nov 2024 09:11:38 +0100 Subject: [PATCH 27/55] Adapt old trivy function to new implementation; #136 --- vars/findVulnerabilitiesWithTrivy.groovy | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) 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 == '') } From 5e5703ee120b63a6079b2ee1cdefddb7c94d95d3 Mon Sep 17 00:00:00 2001 From: Alexander Dammeier Date: Thu, 28 Nov 2024 09:14:48 +0100 Subject: [PATCH 28/55] Revert "#136 use javac compiler as default" This reverts commit e6736d9512cd2dfd088516c0260aecbb6098e8a8. The problem with this commit was, that now the jenkins pipeline only creates empty jars. It only works on local machines when we call our groovy tests directly --- pom.xml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index b8c64f85..44e3eb36 100644 --- a/pom.xml +++ b/pom.xml @@ -98,17 +98,20 @@ org.apache.maven.plugins maven-compiler-plugin - 3.13.0 + 3.8.1 + + groovy-eclipse-compiler + org.codehaus.groovy groovy-eclipse-compiler - 3.7.0 + 3.3.0-01 org.codehaus.groovy groovy-eclipse-batch - 3.0.8-01 + 2.5.6-01 From dc31bd44f0d414c760557e80680c7cde09299ff1 Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Thu, 28 Nov 2024 09:19:56 +0100 Subject: [PATCH 29/55] Update changelog; #136 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 077baca7..97b32197 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ 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. ## [3.1.0](https://github.com/cloudogu/ces-build-lib/releases/tag/3.0.0) - 2024-11-25 ### Added From a4651bd0e849943e8c537e603fb6ec42943c6a6e Mon Sep 17 00:00:00 2001 From: Alexander Dammeier Date: Thu, 28 Nov 2024 09:28:49 +0100 Subject: [PATCH 30/55] #136 compile to java8 #136 update to latest temurin 11 jdk temurin is the successor of openjdk --- Jenkinsfile | 2 +- pom.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 98ae560b..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) diff --git a/pom.xml b/pom.xml index 44e3eb36..b879f221 100644 --- a/pom.xml +++ b/pom.xml @@ -25,8 +25,8 @@ UTF-8 0.8.5 - 11 - 11 + 1.8 + 1.8 From 37ce2f94ddafb2b2b69585fa055ef358632bbe51 Mon Sep 17 00:00:00 2001 From: Alexander Dammeier Date: Thu, 28 Nov 2024 16:47:29 +0100 Subject: [PATCH 31/55] #136 add tests for trivy scans --- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 2 +- .../cloudogu/ces/cesbuildlib/TrivyTest.groovy | 158 +++++++++++++++--- 2 files changed, 139 insertions(+), 21 deletions(-) diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index 2faa82ec..81f9a3da 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -40,7 +40,7 @@ class Trivy implements Serializable { .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 1 if vulnerabilities are found + // 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) } diff --git a/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy b/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy index fe32a3a3..21c65553 100644 --- a/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy +++ b/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy @@ -16,20 +16,15 @@ import static org.mockito.Mockito.when class TrivyTest extends GroovyTestCase { - + 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) void testScanImage_successfulTrivyExecution() { - - Path installDir = Paths.get("target/trivyInstalls") - Path workDir = Paths.get("") - TrivyExecutor trivyExec = new TrivyExecutor(installDir) - - // with hopes that this image will never have CVEs String imageName = "hello-world" String severityLevel = TrivySeverityLevel.CRITICAL - String strategy = TrivyScanStrategy.UNSTABLE - String additionalFlags = "--db-repository public.ecr.aws/aquasecurity/trivy-db --java-db-repository public.ecr.aws/aquasecurity/trivy-java-db" 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}" @@ -64,34 +59,157 @@ class TrivyTest extends GroovyTestCase { fail("terminate trivy due to timeout") } - return statusCode + return expectedStatusCode + } + }) + Trivy trivy = new Trivy(scriptMock, Trivy.DEFAULT_TRIVY_VERSION, dockerMock) + trivy.scanImage(imageName, severityLevel, TrivyScanStrategy.UNSTABLE) + + assertEquals(false, scriptMock.getUnstable()) + } + + void testScanImage_unstableBecauseOfCVEs() { + // with hopes that this image will always have CVEs + String imageName = "alpine:3.18.7" + String severityLevel = TrivySeverityLevel.ALL + 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" + + String trivyImage = "aquasec/trivy:" + Trivy.DEFAULT_TRIVY_VERSION + 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 + Integer expectedStatusCode = 10 + 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, dockerMock) + trivy.scanImage(imageName, severityLevel, TrivyScanStrategy.UNSTABLE) + + assertEquals(true, scriptMock.getUnstable()) + } + + void testScanImage_failBecauseOfCVEs() { + // with hopes that this image will always have CVEs + String imageName = "alpine:3.18.7" + String severityLevel = TrivySeverityLevel.ALL + 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" + + String trivyImage = "aquasec/trivy:" + Trivy.DEFAULT_TRIVY_VERSION + 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 + Integer expectedStatusCode = 10 + Closure closure = invocation.getArgument(1) + scriptMock.expectedShRetValueForScript.put(expectedTrivyCommand, expectedStatusCode) + Integer statusCode = closure.call() as Integer + assertEquals(expectedStatusCode, statusCode) + assertEquals(expectedTrivyCommand, scriptMock.getActualShMapArgs().getLast()) - trivy.scanImage(imageName) + // 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, dockerMock) + def errorMsg = shouldFail { + trivy.scanImage(imageName, severityLevel, TrivyScanStrategy.FAIL) + } + assertTrue("exception is: $errorMsg", errorMsg.contains("Trivy has found vulnerabilities in image")) assertEquals(false, scriptMock.getUnstable()) } void testScanImage_unsuccessfulTrivyExecution() { + // with hopes that this image will always have CVEs + String imageName = "inval!d:::///1.1...1.1." + String severityLevel = TrivySeverityLevel.ALL + 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" + + String trivyImage = "aquasec/trivy:" + Trivy.DEFAULT_TRIVY_VERSION def scriptMock = new ScriptMock() scriptMock.env.WORKSPACE = "/test" Docker dockerMock = mock(Docker.class) Docker.Image imageMock = mock(Docker.Image.class) - when(dockerMock.image("aquasec/trivy:"+Trivy.DEFAULT_TRIVY_VERSION)).thenReturn(imageMock) + 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())).thenThrow(new RuntimeException("Trivy scan had errors: ")) - Trivy trivy = new Trivy(scriptMock, "0.57.1", dockerMock) + 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 + Integer expectedStatusCode = 1 + Closure closure = invocation.getArgument(1) + scriptMock.expectedShRetValueForScript.put(expectedTrivyCommand, expectedStatusCode) + Integer statusCode = closure.call() as Integer + assertEquals(expectedTrivyCommand, scriptMock.getActualShMapArgs().getLast()) + assertEquals(expectedStatusCode, statusCode) - def exception = shouldFail { - trivy.scanImage("inval!d:::///1.1...1.1.") + // 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, dockerMock) + def errorMsg = shouldFail { + trivy.scanImage("inval!d:::///1.1...1.1.", severityLevel, TrivyScanStrategy.UNSTABLE) } - assert exception.contains("Trivy scan had errors: ") + assertTrue("exception is: $errorMsg", errorMsg.contains("Error during trivy scan; exit code: 1")) } - void testSaveFormattedTrivyReport() { - notYetImplemented() - } } From 94ef45fe751ef9befe2cd7dd012c39d2a1451c4f Mon Sep 17 00:00:00 2001 From: Alexander Dammeier Date: Thu, 28 Nov 2024 17:22:54 +0100 Subject: [PATCH 32/55] #136 refactor tests --- .../cloudogu/ces/cesbuildlib/TrivyTest.groovy | 165 ++++-------------- 1 file changed, 37 insertions(+), 128 deletions(-) diff --git a/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy b/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy index 21c65553..849f047d 100644 --- a/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy +++ b/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy @@ -1,6 +1,6 @@ package com.cloudogu.ces.cesbuildlib - +import junit.framework.AssertionFailedError import org.mockito.invocation.InvocationOnMock import org.mockito.stubbing.Answer @@ -20,17 +20,15 @@ class TrivyTest extends GroovyTestCase { Path installDir = Paths.get("target/trivyInstalls") Path workDir = Paths.get("") TrivyExecutor trivyExec = new TrivyExecutor(installDir) + String trivyImage = "aquasec/trivy:" + Trivy.DEFAULT_TRIVY_VERSION - void testScanImage_successfulTrivyExecution() { - // with hopes that this image will never have CVEs - String imageName = "hello-world" - String severityLevel = TrivySeverityLevel.CRITICAL + + 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" - String trivyImage = "aquasec/trivy:" + Trivy.DEFAULT_TRIVY_VERSION def scriptMock = new ScriptMock() scriptMock.env.WORKSPACE = "/test" Docker dockerMock = mock(Docker.class) @@ -42,7 +40,6 @@ class TrivyTest extends GroovyTestCase { @Override Integer answer(InvocationOnMock invocation) throws Throwable { // mock "sh trivy" so that it returns the expected status code and check trivy arguments - Integer expectedStatusCode = 0 Closure closure = invocation.getArgument(1) scriptMock.expectedShRetValueForScript.put(expectedTrivyCommand, expectedStatusCode) Integer statusCode = closure.call() as Integer @@ -63,7 +60,18 @@ class TrivyTest extends GroovyTestCase { } }) Trivy trivy = new Trivy(scriptMock, Trivy.DEFAULT_TRIVY_VERSION, dockerMock) - trivy.scanImage(imageName, severityLevel, TrivyScanStrategy.UNSTABLE) + + trivy.scanImage(imageName, severityLevel, strategy) + + return scriptMock + } + + 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()) } @@ -72,45 +80,8 @@ class TrivyTest extends GroovyTestCase { // with hopes that this image will always have CVEs String imageName = "alpine:3.18.7" String severityLevel = TrivySeverityLevel.ALL - 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" - String trivyImage = "aquasec/trivy:" + Trivy.DEFAULT_TRIVY_VERSION - 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 - Integer expectedStatusCode = 10 - 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, dockerMock) - trivy.scanImage(imageName, severityLevel, TrivyScanStrategy.UNSTABLE) + def scriptMock = doTestScan(imageName, severityLevel, TrivyScanStrategy.UNSTABLE, 10) assertEquals(true, scriptMock.getUnstable()) } @@ -119,97 +90,35 @@ class TrivyTest extends GroovyTestCase { // with hopes that this image will always have CVEs String imageName = "alpine:3.18.7" String severityLevel = TrivySeverityLevel.ALL - 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" - - String trivyImage = "aquasec/trivy:" + Trivy.DEFAULT_TRIVY_VERSION - 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 - Integer expectedStatusCode = 10 - 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, dockerMock) - def errorMsg = shouldFail { - trivy.scanImage(imageName, severityLevel, TrivyScanStrategy.FAIL) + def gotException = false + try { + doTestScan(imageName, severityLevel, TrivyScanStrategy.FAIL, 10)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("exception is: ${e.getMessage()}", e.getMessage().contains("Trivy has found vulnerabilities in image")) + gotException = true } - assertTrue("exception is: $errorMsg", errorMsg.contains("Trivy has found vulnerabilities in image")) - assertEquals(false, scriptMock.getUnstable()) + assertTrue(gotException) } void testScanImage_unsuccessfulTrivyExecution() { // with hopes that this image will always have CVEs String imageName = "inval!d:::///1.1...1.1." String severityLevel = TrivySeverityLevel.ALL - 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" - String trivyImage = "aquasec/trivy:" + Trivy.DEFAULT_TRIVY_VERSION - 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 - Integer expectedStatusCode = 1 - Closure closure = invocation.getArgument(1) - scriptMock.expectedShRetValueForScript.put(expectedTrivyCommand, expectedStatusCode) - Integer statusCode = closure.call() as Integer - assertEquals(expectedTrivyCommand, scriptMock.getActualShMapArgs().getLast()) - assertEquals(expectedStatusCode, statusCode) - - // 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, dockerMock) - def errorMsg = shouldFail { - trivy.scanImage("inval!d:::///1.1...1.1.", severityLevel, TrivyScanStrategy.UNSTABLE) + 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("exception is: ${e.getMessage()}", e.getMessage().contains("Error during trivy scan; exit code: 1")) + gotException = true } - assertTrue("exception is: $errorMsg", errorMsg.contains("Error during trivy scan; exit code: 1")) + assertTrue(gotException) } - } From a8cbc915f6ee22b1067bd6e6cbd77402ea4ea1f4 Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Tue, 10 Dec 2024 11:17:46 +0100 Subject: [PATCH 33/55] Update src/com/cloudogu/ces/cesbuildlib/TrivySeverityLevel.groovy Co-authored-by: Alexander Dammeier <55015726+alexander-dammeier@users.noreply.github.com> --- src/com/cloudogu/ces/cesbuildlib/TrivySeverityLevel.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/com/cloudogu/ces/cesbuildlib/TrivySeverityLevel.groovy b/src/com/cloudogu/ces/cesbuildlib/TrivySeverityLevel.groovy index 3d780621..8759bd0d 100644 --- a/src/com/cloudogu/ces/cesbuildlib/TrivySeverityLevel.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/TrivySeverityLevel.groovy @@ -12,12 +12,12 @@ class TrivySeverityLevel { /** * High or critical vulnerabilities. */ - static String HIGH = "CRITICAL,HIGH" + static String HIGH_AND_ABOVE = "CRITICAL,HIGH" /** * Medium or higher vulnerabilities. */ - static String MEDIUM = "CRITICAL,HIGH,MEDIUM" + static String MEDIUM_AND_ABOVE = "CRITICAL,HIGH,MEDIUM" /** * All vulnerabilities. From e1d03340967a115a9a3fa82776f16f642ee6ff91 Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Tue, 10 Dec 2024 11:22:42 +0100 Subject: [PATCH 34/55] Clarify additional flags defaults; #136 --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d5097d8d..3d0cf71d 100644 --- a/README.md +++ b/README.md @@ -1303,7 +1303,8 @@ trivy.scanImage("ubuntu:24.04", TrivySeverityLevel.ALL, TrivyScanStrategy.UNSTAB ``` 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. +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 From c8484a0d66ddae1db9807d85726080c2fc212b22 Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Tue, 10 Dec 2024 11:33:17 +0100 Subject: [PATCH 35/55] Do not potentially overwrite the original trivy report file; #136 --- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index 81f9a3da..bb0971d2 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -103,7 +103,7 @@ class Trivy implements Serializable { * @param formattedTrivyReportFilename The file name your report files should get, without file extension. E.g. "ubuntu24report" * @param trivyReportFile The "trivyReportFile" parameter you used in the "scanImage" function, if it was set */ - void saveFormattedTrivyReport(String format = TrivyScanFormat.HTML, String formattedTrivyReportFilename = "trivyReport", String trivyReportFile = "trivy/trivyReport.json") { + void saveFormattedTrivyReport(String format = TrivyScanFormat.HTML, String formattedTrivyReportFilename = "formattedTrivyReport", String trivyReportFile = "trivy/trivyReport.json") { String fileExtension String formatString String trivyDirectory = "trivy/" From 12c2a304bb73da8303dd04ae70ac57ef45ce6a57 Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Tue, 10 Dec 2024 11:37:42 +0100 Subject: [PATCH 36/55] Use class variable instead of hardcoding the directory; #136 --- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index bb0971d2..15c33e9a 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -106,7 +106,6 @@ class Trivy implements Serializable { void saveFormattedTrivyReport(String format = TrivyScanFormat.HTML, String formattedTrivyReportFilename = "formattedTrivyReport", String trivyReportFile = "trivy/trivyReport.json") { String fileExtension String formatString - String trivyDirectory = "trivy/" switch (format) { case TrivyScanFormat.HTML: formatString = "template --template \"@/contrib/html.tpl\"" @@ -126,7 +125,7 @@ class Trivy implements Serializable { } docker.image("aquasec/trivy:${trivyVersion}") .inside("-v ${script.env.WORKSPACE}/.trivy/.cache:/root/.cache/") { - script.sh(script: "trivy convert --format ${formatString} --output ${trivyDirectory}${formattedTrivyReportFilename}.${fileExtension} ${trivyReportFile}") + script.sh(script: "trivy convert --format ${formatString} --output ${trivyDirectory}/${formattedTrivyReportFilename}.${fileExtension} ${trivyReportFile}") } script.archiveArtifacts artifacts: "${trivyDirectory}${formattedTrivyReportFilename}.*", allowEmptyArchive: true } From 69b92099acf57764f69a96460ffc9c16405b15de Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Tue, 10 Dec 2024 11:38:44 +0100 Subject: [PATCH 37/55] Fix path; #136 --- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index 15c33e9a..08497c71 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -127,6 +127,6 @@ class Trivy implements Serializable { .inside("-v ${script.env.WORKSPACE}/.trivy/.cache:/root/.cache/") { script.sh(script: "trivy convert --format ${formatString} --output ${trivyDirectory}/${formattedTrivyReportFilename}.${fileExtension} ${trivyReportFile}") } - script.archiveArtifacts artifacts: "${trivyDirectory}${formattedTrivyReportFilename}.*", allowEmptyArchive: true + script.archiveArtifacts artifacts: "${trivyDirectory}/${formattedTrivyReportFilename}.*", allowEmptyArchive: true } } From cd604e834ad121321c5eda5362a42d62bccdcb15 Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Tue, 10 Dec 2024 11:47:45 +0100 Subject: [PATCH 38/55] Remove outdated comments; #136 --- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index 08497c71..58a44a29 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -18,7 +18,6 @@ class Trivy implements Serializable { * 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() - * - Evaluate via exit codes: 0 = no vulnerability; 1 = vulnerabilities found; other = function call failed * * @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') @@ -74,7 +73,6 @@ class Trivy implements Serializable { * 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() - * - Evaluate via exit codes: 0 = no vulnerability; 1 = vulnerabilities found; other = function call failed * * @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') From 174645f059efb50fda1f4606d296bf964992382a Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Tue, 10 Dec 2024 13:49:38 +0100 Subject: [PATCH 39/55] Make trivy image source adjustable; #136 --- README.md | 4 +++- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 11 +++++++---- test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3d0cf71d..a7cfed0b 100644 --- a/README.md +++ b/README.md @@ -1251,10 +1251,12 @@ Scan container images for vulnerabilities with Trivy. 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", docker) +Trivy trivy = new Trivy(this, "0.57.1", "aquasec/trivy", docker) ``` ## Scan image with Trivy diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index 58a44a29..5499922f 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -2,14 +2,17 @@ package com.cloudogu.ces.cesbuildlib 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" - Trivy(script, String trivyVersion = "0.57.1", Docker docker = new Docker(script)) { + 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 } @@ -34,12 +37,12 @@ class Trivy implements Serializable { 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("aquasec/trivy:${trivyVersion}") + 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 + // 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) } @@ -121,7 +124,7 @@ class Trivy implements Serializable { script.error("This format did not match the supported formats: " + format) return } - docker.image("aquasec/trivy:${trivyVersion}") + docker.image("${trivyImage}:${trivyVersion}") .inside("-v ${script.env.WORKSPACE}/.trivy/.cache:/root/.cache/") { script.sh(script: "trivy convert --format ${formatString} --output ${trivyDirectory}/${formattedTrivyReportFilename}.${fileExtension} ${trivyReportFile}") } diff --git a/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy b/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy index 849f047d..ab98f85f 100644 --- a/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy +++ b/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy @@ -59,7 +59,7 @@ class TrivyTest extends GroovyTestCase { return expectedStatusCode } }) - Trivy trivy = new Trivy(scriptMock, Trivy.DEFAULT_TRIVY_VERSION, dockerMock) + Trivy trivy = new Trivy(scriptMock, Trivy.DEFAULT_TRIVY_VERSION, Trivy.DEFAULT_TRIVY_IMAGE, dockerMock) trivy.scanImage(imageName, severityLevel, strategy) From 15fe1a967a9685f3fa12b7706aea19f36eba3d66 Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Wed, 11 Dec 2024 14:09:14 +0100 Subject: [PATCH 40/55] Enable custom formats for Trivy report conversion; #136 --- README.md | 10 ++++++++++ src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 18 +++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a7cfed0b..28dfb422 100644 --- a/README.md +++ b/README.md @@ -1332,6 +1332,16 @@ trivy.saveFormattedTrivyReport(TrivyScanFormat.JSON) trivy.saveFormattedTrivyReport(TrivyScanFormat.HTML) ``` +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. +The output file of this converted Trivy report will have the extension "custom". + +```groovy +Trivy trivy = new Trivy(this) +trivy.scanImage("ubuntu:24.04") +trivy.saveFormattedTrivyReport("cosign-vuln") +trivy.saveFormattedTrivyReport("template --template @myTemplateFile.xyz") +``` + ## Scan Dogu image with Trivy The `scanDogu()` function lets you scan a Dogu image without typing its full name. The method reads the image name diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index 5499922f..7f342790 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -42,7 +42,7 @@ class Trivy implements Serializable { .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 + // 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) } @@ -121,8 +121,20 @@ class Trivy implements Serializable { fileExtension = "txt" break default: - script.error("This format did not match the supported formats: " + format) - return + // 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.contains(it) } || isTemplateFormat) { + formatString = format + fileExtension = "custom" + break + } else { + script.error("This format did not match the supported formats: " + format) + return + } } docker.image("${trivyImage}:${trivyVersion}") .inside("-v ${script.env.WORKSPACE}/.trivy/.cache:/root/.cache/") { From aecb7fd5057ae680722a5d2b0892610d8693a73b Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Wed, 11 Dec 2024 14:42:42 +0100 Subject: [PATCH 41/55] Do not overwrite first report file on second conversion; #136 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 28dfb422..9b52ad61 100644 --- a/README.md +++ b/README.md @@ -1338,8 +1338,8 @@ The output file of this converted Trivy report will have the extension "custom". ```groovy Trivy trivy = new Trivy(this) trivy.scanImage("ubuntu:24.04") -trivy.saveFormattedTrivyReport("cosign-vuln") -trivy.saveFormattedTrivyReport("template --template @myTemplateFile.xyz") +trivy.saveFormattedTrivyReport("cosign-vuln", "ubuntu24.04cosign") +trivy.saveFormattedTrivyReport("template --template @myTemplateFile.xyz", "ubuntu24.04myTemplate") ``` ## Scan Dogu image with Trivy From d2cb3a7e99fbb0bb3c439f2ab1e9594186b88707 Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Thu, 19 Dec 2024 09:08:02 +0100 Subject: [PATCH 42/55] Remove faulty code --- test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy b/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy index ab98f85f..55449468 100644 --- a/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy +++ b/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy @@ -93,7 +93,7 @@ class TrivyTest extends GroovyTestCase { def gotException = false try { - doTestScan(imageName, severityLevel, TrivyScanStrategy.FAIL, 10)doTestScan(imageName, severityLevel, TrivyScanStrategy.FAIL, 10) + 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 @@ -105,7 +105,6 @@ class TrivyTest extends GroovyTestCase { } void testScanImage_unsuccessfulTrivyExecution() { - // with hopes that this image will always have CVEs String imageName = "inval!d:::///1.1...1.1." String severityLevel = TrivySeverityLevel.ALL From e4ea85719b382c679b2419fa279d7d745ee1cae3 Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Thu, 19 Dec 2024 09:14:03 +0100 Subject: [PATCH 43/55] Let the user decide which file extension to use; #136 --- README.md | 5 ++--- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 18 ++++++++++++------ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 9b52ad61..92dbb9f1 100644 --- a/README.md +++ b/README.md @@ -1333,13 +1333,12 @@ trivy.saveFormattedTrivyReport(TrivyScanFormat.HTML) ``` 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. -The output file of this converted Trivy report will have the extension "custom". ```groovy Trivy trivy = new Trivy(this) trivy.scanImage("ubuntu:24.04") -trivy.saveFormattedTrivyReport("cosign-vuln", "ubuntu24.04cosign") -trivy.saveFormattedTrivyReport("template --template @myTemplateFile.xyz", "ubuntu24.04myTemplate") +trivy.saveFormattedTrivyReport("cosign-vuln", "ubuntu24.04cosign.txt") +trivy.saveFormattedTrivyReport("template --template @myTemplateFile.xyz", "ubuntu24.04myTemplate.txt") ``` ## Scan Dogu image with Trivy diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index 7f342790..efe7ca95 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -104,21 +104,27 @@ class Trivy implements Serializable { * @param formattedTrivyReportFilename The file name your report files should get, without file extension. E.g. "ubuntu24report" * @param trivyReportFile The "trivyReportFile" parameter you used in the "scanImage" function, if it was set */ - void saveFormattedTrivyReport(String format = TrivyScanFormat.HTML, String formattedTrivyReportFilename = "formattedTrivyReport", String trivyReportFile = "trivy/trivyReport.json") { - String fileExtension + void saveFormattedTrivyReport(String format = TrivyScanFormat.HTML, String formattedTrivyReportFilename = "formattedTrivyReport.txt", String trivyReportFile = "trivy/trivyReport.json") { String formatString + String defaultFilename = "formattedTrivyReport.txt" switch (format) { case TrivyScanFormat.HTML: formatString = "template --template \"@/contrib/html.tpl\"" - fileExtension = "html" + if (formattedTrivyReportFilename == defaultFilename) { + formattedTrivyReportFilename == "formattedTrivyReport.html" + } break case TrivyScanFormat.JSON: formatString = "json" - fileExtension = "json" + if (formattedTrivyReportFilename == defaultFilename) { + formattedTrivyReportFilename == "formattedTrivyReport.json" + } break case TrivyScanFormat.TABLE: formatString = "table" - fileExtension = "txt" + if (formattedTrivyReportFilename == defaultFilename) { + formattedTrivyReportFilename == "formattedTrivyReport.table" + } break default: // You may enter supported formats (sarif, cyclonedx, spdx, spdx-json, github, cosign-vuln, table or json) @@ -138,7 +144,7 @@ class Trivy implements Serializable { } docker.image("${trivyImage}:${trivyVersion}") .inside("-v ${script.env.WORKSPACE}/.trivy/.cache:/root/.cache/") { - script.sh(script: "trivy convert --format ${formatString} --output ${trivyDirectory}/${formattedTrivyReportFilename}.${fileExtension} ${trivyReportFile}") + script.sh(script: "trivy convert --format ${formatString} --output ${trivyDirectory}/${formattedTrivyReportFilename} ${trivyReportFile}") } script.archiveArtifacts artifacts: "${trivyDirectory}/${formattedTrivyReportFilename}.*", allowEmptyArchive: true } From 790c557625d97aa97141ba62d6720e3dcaa034de Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Thu, 19 Dec 2024 09:43:05 +0100 Subject: [PATCH 44/55] Select specific severity levels in formatted report; #136 --- README.md | 15 ++++++++++++--- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 12 ++++++++++-- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 92dbb9f1..e00962f4 100644 --- a/README.md +++ b/README.md @@ -1317,7 +1317,7 @@ 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, "ubuntu20.04report", "trivy/ubuntu20.json") +trivy.saveFormattedTrivyReport(TrivyScanFormat.HTML, "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL", "ubuntu20.04report", "trivy/ubuntu20.json") ``` ## Save Trivy report in another file format @@ -1332,13 +1332,22 @@ 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", "ubuntu24.04cosign.txt") -trivy.saveFormattedTrivyReport("template --template @myTemplateFile.xyz", "ubuntu24.04myTemplate.txt") +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 diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index efe7ca95..c219a98d 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -101,11 +101,13 @@ class Trivy implements Serializable { * Save the Trivy scan results as a file with a specific format * * @param format The format of the output file (@see TrivyScanFormat) + * @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, without file extension. E.g. "ubuntu24report" * @param trivyReportFile The "trivyReportFile" parameter you used in the "scanImage" function, if it was set */ - void saveFormattedTrivyReport(String format = TrivyScanFormat.HTML, String formattedTrivyReportFilename = "formattedTrivyReport.txt", String trivyReportFile = "trivy/trivyReport.json") { + void saveFormattedTrivyReport(String format = TrivyScanFormat.HTML, String severity = "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL", String formattedTrivyReportFilename = "formattedTrivyReport.txt", String trivyReportFile = "trivy/trivyReport.json") { String formatString + String defaultSeverityLevels = "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL" String defaultFilename = "formattedTrivyReport.txt" switch (format) { case TrivyScanFormat.HTML: @@ -142,9 +144,15 @@ class Trivy implements Serializable { return } } + // Validate severity input parameter to prevent injection of additional parameters + if (severity != defaultSeverityLevels) { + 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} --output ${trivyDirectory}/${formattedTrivyReportFilename} ${trivyReportFile}") + script.sh(script: "trivy convert --format ${formatString} --severity ${severity} --output ${trivyDirectory}/${formattedTrivyReportFilename} ${trivyReportFile}") } script.archiveArtifacts artifacts: "${trivyDirectory}/${formattedTrivyReportFilename}.*", allowEmptyArchive: true } From 2bf56652f6f1109fa4c98d73aae89a852332bc3c Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Thu, 19 Dec 2024 09:51:50 +0100 Subject: [PATCH 45/55] Remove unused variable assignment; #136 --- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 1 - 1 file changed, 1 deletion(-) diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index c219a98d..d50fb7dc 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -137,7 +137,6 @@ class Trivy implements Serializable { // Check if "format" is one of the trivyFormats or a template if (trivyFormats.any { format.contains(it) } || isTemplateFormat) { formatString = format - fileExtension = "custom" break } else { script.error("This format did not match the supported formats: " + format) From 60eb8f1d4202ed82bc1e9536bdeaecc4c89eedd8 Mon Sep 17 00:00:00 2001 From: meiserloh Date: Fri, 20 Dec 2024 16:00:17 +0100 Subject: [PATCH 46/55] #136 - refactor saveFormattedTrivyReport --- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 55 ++++++++++--------- .../ces/cesbuildlib/TrivyScanFormat.groovy | 4 ++ .../cloudogu/ces/cesbuildlib/TrivyTest.groovy | 39 ++++++++++++- 3 files changed, 70 insertions(+), 28 deletions(-) diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index d50fb7dc..9913d4bf 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -9,7 +9,7 @@ class Trivy implements Serializable { private String trivyImage private String trivyDirectory = "trivy" - Trivy(script, String trivyVersion = "0.57.1", String trivyImage = "aquasec/trivy", Docker docker = new Docker(script)) { + Trivy(script, String trivyVersion = DEFAULT_TRIVY_VERSION, String trivyImage = DEFAULT_TRIVY_IMAGE, Docker docker = new Docker(script)) { this.script = script this.trivyVersion = trivyVersion this.trivyImage = trivyImage @@ -94,39 +94,37 @@ class Trivy implements Serializable { ) { 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) + 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 (@see TrivyScanFormat) + * @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, without file extension. E.g. "ubuntu24report" + * @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 = "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL", String formattedTrivyReportFilename = "formattedTrivyReport.txt", String trivyReportFile = "trivy/trivyReport.json") { + void saveFormattedTrivyReport(String format = TrivyScanFormat.HTML, + String severity = "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL", + 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 - String defaultSeverityLevels = "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL" - String defaultFilename = "formattedTrivyReport.txt" switch (format) { + // TrivyScanFormat.JSON and TrivyScanFormat.TABLE are handled by the default case, too case TrivyScanFormat.HTML: formatString = "template --template \"@/contrib/html.tpl\"" - if (formattedTrivyReportFilename == defaultFilename) { - formattedTrivyReportFilename == "formattedTrivyReport.html" - } - break - case TrivyScanFormat.JSON: - formatString = "json" - if (formattedTrivyReportFilename == defaultFilename) { - formattedTrivyReportFilename == "formattedTrivyReport.json" - } - break - case TrivyScanFormat.TABLE: - formatString = "table" - if (formattedTrivyReportFilename == defaultFilename) { - formattedTrivyReportFilename == "formattedTrivyReport.table" - } break default: // You may enter supported formats (sarif, cyclonedx, spdx, spdx-json, github, cosign-vuln, table or json) @@ -135,7 +133,7 @@ class Trivy implements Serializable { // 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.contains(it) } || isTemplateFormat) { + if (trivyFormats.any { (format == it) } || isTemplateFormat) { formatString = format break } else { @@ -144,15 +142,18 @@ class Trivy implements Serializable { } } // Validate severity input parameter to prevent injection of additional parameters - if (severity != defaultSeverityLevels) { - 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).") - } + 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/TrivyScanFormat.groovy b/src/com/cloudogu/ces/cesbuildlib/TrivyScanFormat.groovy index c2131ebf..d91c184b 100644 --- a/src/com/cloudogu/ces/cesbuildlib/TrivyScanFormat.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/TrivyScanFormat.groovy @@ -18,4 +18,8 @@ class TrivyScanFormat { * Output as table. */ static String TABLE = "table" + + static boolean isStandardScanFormat(String format) { + return format == HTML || format == JSON || format == TABLE + } } diff --git a/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy b/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy index 55449468..65d5621d 100644 --- a/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy +++ b/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy @@ -49,7 +49,7 @@ class TrivyTest extends GroovyTestCase { // 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)) { + if (process.waitFor(2, TimeUnit.MINUTES)) { assertEquals(expectedStatusCode, process.exitValue()) } else { process.destroyForcibly() @@ -120,4 +120,41 @@ class TrivyTest extends GroovyTestCase { } assertTrue(gotException) } + + void testSaveFormattedTrivyReport() { + ScriptMock scriptMock = mockSaveFormattedTrivyReport( + "template --template \"@/contrib/html.tpl\"", + "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL", + "trivy/formattedTrivyReport.html") + + println(scriptMock.archivedArtifacts) + assertFalse(true) + } + + ScriptMock mockSaveFormattedTrivyReport(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()) + println(scriptMock.getActualShMapArgs().getLast()) + return 0 + } + }) + Trivy trivy = new Trivy(scriptMock, Trivy.DEFAULT_TRIVY_VERSION, Trivy.DEFAULT_TRIVY_IMAGE, dockerMock) + trivy.saveFormattedTrivyReport() + + return scriptMock + } } From 59946eef17ed785a8ff9664d5e3023fd4c386583 Mon Sep 17 00:00:00 2001 From: meiserloh Date: Fri, 20 Dec 2024 16:44:04 +0100 Subject: [PATCH 47/55] #136 - change Test to Junit5 --- test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy b/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy index 65d5621d..8e3c12c4 100644 --- a/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy +++ b/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy @@ -1,6 +1,7 @@ package com.cloudogu.ces.cesbuildlib import junit.framework.AssertionFailedError +import org.junit.jupiter.api.Test import org.mockito.invocation.InvocationOnMock import org.mockito.stubbing.Answer @@ -9,12 +10,13 @@ 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 extends GroovyTestCase { +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") @@ -66,6 +68,7 @@ class TrivyTest extends GroovyTestCase { return scriptMock } + @Test void testScanImage_successfulTrivyExecution() { // with hopes that this image will never have CVEs String imageName = "hello-world" @@ -76,6 +79,7 @@ class TrivyTest extends GroovyTestCase { assertEquals(false, scriptMock.getUnstable()) } + @Test void testScanImage_unstableBecauseOfCVEs() { // with hopes that this image will always have CVEs String imageName = "alpine:3.18.7" @@ -86,6 +90,7 @@ class TrivyTest extends GroovyTestCase { assertEquals(true, scriptMock.getUnstable()) } + @Test void testScanImage_failBecauseOfCVEs() { // with hopes that this image will always have CVEs String imageName = "alpine:3.18.7" @@ -98,12 +103,13 @@ class TrivyTest extends GroovyTestCase { // exception could also be a junit assertion exception. This means a previous assertion failed throw e } catch (Exception e) { - assertTrue("exception is: ${e.getMessage()}", e.getMessage().contains("Trivy has found vulnerabilities in image")) + 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 @@ -115,12 +121,13 @@ class TrivyTest extends GroovyTestCase { // exception could also be a junit assertion exception. This means a previous assertion failed throw e } catch (Exception e) { - assertTrue("exception is: ${e.getMessage()}", e.getMessage().contains("Error during trivy scan; exit code: 1")) + assertTrue(e.getMessage().contains("Error during trivy scan; exit code: 1"), "exception is: ${e.getMessage()}") gotException = true } assertTrue(gotException) } + @Test void testSaveFormattedTrivyReport() { ScriptMock scriptMock = mockSaveFormattedTrivyReport( "template --template \"@/contrib/html.tpl\"", @@ -128,7 +135,6 @@ class TrivyTest extends GroovyTestCase { "trivy/formattedTrivyReport.html") println(scriptMock.archivedArtifacts) - assertFalse(true) } ScriptMock mockSaveFormattedTrivyReport(String expectedFormat, String expectedSeverity, String expectedOutput) { From bcb0de2cb14bc63791b79bed4a75199db38dd595 Mon Sep 17 00:00:00 2001 From: meiserloh Date: Thu, 2 Jan 2025 10:56:26 +0100 Subject: [PATCH 48/55] #136 - add tests for saveFormattedTrivyReport --- .../cloudogu/ces/cesbuildlib/TrivyTest.groovy | 85 +++++++++++++++++-- 1 file changed, 77 insertions(+), 8 deletions(-) diff --git a/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy b/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy index 8e3c12c4..bd9b4970 100644 --- a/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy +++ b/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy @@ -128,16 +128,88 @@ class TrivyTest { } @Test - void testSaveFormattedTrivyReport() { - ScriptMock scriptMock = mockSaveFormattedTrivyReport( + 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) + } - println(scriptMock.archivedArtifacts) + @Test + void testSaveFormattedTrivyReport_CustomFilename() { + Trivy trivy = mockTrivy( + "json", + "CRITICAL,HIGH,MEDIUM", + "trivy/myOutput.custom") + trivy.saveFormattedTrivyReport(TrivyScanFormat.JSON, TrivySeverityLevel.MEDIUM_AND_ABOVE, "myOutput.custom") } - ScriptMock mockSaveFormattedTrivyReport(String expectedFormat, String expectedSeverity, String expectedOutput) { + @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" @@ -154,13 +226,10 @@ class TrivyTest { scriptMock.expectedShRetValueForScript.put(expectedTrivyCommand, 0) closure.call() assertEquals(expectedTrivyCommand, scriptMock.getActualShMapArgs().getLast()) - println(scriptMock.getActualShMapArgs().getLast()) return 0 } }) Trivy trivy = new Trivy(scriptMock, Trivy.DEFAULT_TRIVY_VERSION, Trivy.DEFAULT_TRIVY_IMAGE, dockerMock) - trivy.saveFormattedTrivyReport() - - return scriptMock + return trivy } } From 640b2a69f63178f35cc22a0c6e6c80f37d984c65 Mon Sep 17 00:00:00 2001 From: meiserloh Date: Mon, 6 Jan 2025 11:18:15 +0100 Subject: [PATCH 49/55] #136 - remove constructor call from parameters to (hopefully) resolve Jenkins issues --- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index 9913d4bf..4aa78b0f 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -9,11 +9,11 @@ class Trivy implements Serializable { private String trivyImage private String trivyDirectory = "trivy" - Trivy(script, String trivyVersion = DEFAULT_TRIVY_VERSION, String trivyImage = DEFAULT_TRIVY_IMAGE, Docker docker = new Docker(script)) { + Trivy(script, String trivyVersion = DEFAULT_TRIVY_VERSION, String trivyImage = DEFAULT_TRIVY_IMAGE, Docker docker = null) { this.script = script this.trivyVersion = trivyVersion this.trivyImage = trivyImage - this.docker = docker + this.docker = docker ?: new Docker(script) } /** From 5b5319839a397b29ce4f120a6655bc5cd46fc4ef Mon Sep 17 00:00:00 2001 From: meiserloh Date: Mon, 6 Jan 2025 13:28:57 +0100 Subject: [PATCH 50/55] #136 - Mark function as NonCPS, because of the use of Iterators --- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index 4aa78b0f..75d2e7ac 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -1,5 +1,7 @@ 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" @@ -9,11 +11,11 @@ class Trivy implements Serializable { private String trivyImage private String trivyDirectory = "trivy" - Trivy(script, String trivyVersion = DEFAULT_TRIVY_VERSION, String trivyImage = DEFAULT_TRIVY_IMAGE, Docker docker = null) { + Trivy(script, String trivyVersion = DEFAULT_TRIVY_VERSION, String trivyImage = DEFAULT_TRIVY_IMAGE, Docker docker = new Docker(script)) { this.script = script this.trivyVersion = trivyVersion this.trivyImage = trivyImage - this.docker = docker ?: new Docker(script) + this.docker = docker } /** @@ -110,6 +112,7 @@ class Trivy implements Serializable { * @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 */ + @NonCPS void saveFormattedTrivyReport(String format = TrivyScanFormat.HTML, String severity = "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL", String formattedTrivyReportFilename = null, From 095f4b784a0e40c43a58be5d5dbdeaf8e07ee26a Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Mon, 6 Jan 2025 14:26:35 +0100 Subject: [PATCH 51/55] Use duplicated strings to prevent VerifyError --- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index 75d2e7ac..3e4f16c6 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -11,7 +11,8 @@ class Trivy implements Serializable { private String trivyImage private String trivyDirectory = "trivy" - Trivy(script, String trivyVersion = DEFAULT_TRIVY_VERSION, String trivyImage = DEFAULT_TRIVY_IMAGE, Docker docker = new Docker(script)) { + // 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 From cc742c0bb86cf160579480263ecc106c7f984e3a Mon Sep 17 00:00:00 2001 From: meiserloh Date: Mon, 6 Jan 2025 14:49:34 +0100 Subject: [PATCH 52/55] #136 - Downgrade Junit & Mockito to be inline with the used JUnit version (coming from groovy-test) --- pom.xml | 24 ++++- .../ces/cesbuildlib/GradleTest.groovy | 2 +- .../cesbuildlib/MavenInDockerBaseTest.groovy | 2 +- .../cloudogu/ces/cesbuildlib/MavenTest.groovy | 87 +++++++++---------- .../ces/cesbuildlib/SCMManagerTest.groovy | 8 +- .../ces/cesbuildlib/SonarCloudTest.groovy | 2 +- .../cloudogu/ces/cesbuildlib/TrivyTest.groovy | 2 +- 7 files changed, 69 insertions(+), 58 deletions(-) diff --git a/pom.xml b/pom.xml index 25766844..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 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/TrivyTest.groovy b/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy index bd9b4970..2557766f 100644 --- a/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy +++ b/test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy @@ -1,9 +1,9 @@ package com.cloudogu.ces.cesbuildlib -import junit.framework.AssertionFailedError 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 From 08fe9668b4af578b78b60e97a7974290fcf132a8 Mon Sep 17 00:00:00 2001 From: meiserloh Date: Tue, 7 Jan 2025 09:44:48 +0100 Subject: [PATCH 53/55] #136 - apply review suggestions --- README.md | 2 +- src/com/cloudogu/ces/cesbuildlib/Trivy.groovy | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e00962f4..f1dc4a95 100644 --- a/README.md +++ b/README.md @@ -1304,7 +1304,7 @@ 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" +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. diff --git a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy index 3e4f16c6..3e52fed5 100644 --- a/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy +++ b/src/com/cloudogu/ces/cesbuildlib/Trivy.groovy @@ -113,9 +113,8 @@ class Trivy implements Serializable { * @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 */ - @NonCPS void saveFormattedTrivyReport(String format = TrivyScanFormat.HTML, - String severity = "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL", + String severity = TrivySeverityLevel.ALL, String formattedTrivyReportFilename = null, String trivyReportFile = "trivy/trivyReport.json") { From 5af485c859d65b47407241c69b3cbe70c8835730 Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Tue, 7 Jan 2025 09:58:19 +0100 Subject: [PATCH 54/55] Highlight technical text --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f1dc4a95..066750fd 100644 --- a/README.md +++ b/README.md @@ -1332,7 +1332,7 @@ 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"): +You may filter the output to show only specific severity levels (default: `"UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL"`): ```groovy Trivy trivy = new Trivy(this) @@ -1370,9 +1370,10 @@ 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.: +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 From a8f00bf0eeb3265113754641a63064dc88523c98 Mon Sep 17 00:00:00 2001 From: Robert Auer Date: Tue, 7 Jan 2025 10:40:16 +0100 Subject: [PATCH 55/55] Describe how to retrieve the dogu image to scan --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 066750fd..29ac16a3 100644 --- a/README.md +++ b/README.md @@ -1352,12 +1352,27 @@ trivy.saveFormattedTrivyReport("template --template @myTemplateFile.xyz", "UNKNO ## 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")