Skip to content

Commit

Permalink
#136 implement trivy integration test
Browse files Browse the repository at this point in the history
  • Loading branch information
robertauer authored and alexander-dammeier committed Nov 27, 2024
1 parent 122c87d commit 326db5c
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 8 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
/*.iml
/.idea/
.mvn/**
**/trivyReport.json
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
15 changes: 15 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jacoco.version>0.8.5</jacoco.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>

<dependencies>
Expand Down Expand Up @@ -68,6 +70,19 @@
<scope>test</scope>
</dependency>


<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.18.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.27.1</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
5 changes: 2 additions & 3 deletions src/com/cloudogu/ces/cesbuildlib/Trivy.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
101 changes: 101 additions & 0 deletions test/com/cloudogu/ces/cesbuildlib/TrivyExecutor.groovy
Original file line number Diff line number Diff line change
@@ -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<String> 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()
}
}
}
57 changes: 53 additions & 4 deletions test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy
Original file line number Diff line number Diff line change
@@ -1,24 +1,73 @@
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
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<Integer>() {
@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())
}
Expand Down

0 comments on commit 326db5c

Please sign in to comment.