Skip to content

Commit

Permalink
#136 add tests for trivy scans
Browse files Browse the repository at this point in the history
  • Loading branch information
alexander-dammeier committed Nov 28, 2024
1 parent 57db423 commit 37ce2f9
Show file tree
Hide file tree
Showing 2 changed files with 139 additions and 21 deletions.
2 changes: 1 addition & 1 deletion src/com/cloudogu/ces/cesbuildlib/Trivy.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
158 changes: 138 additions & 20 deletions test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down Expand Up @@ -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<Integer>() {
@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<Integer>() {
@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<Integer>() {
@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()
}
}

0 comments on commit 37ce2f9

Please sign in to comment.