Skip to content

Commit 60eb8f1

Browse files
committed
#136 - refactor saveFormattedTrivyReport
1 parent 2bf5665 commit 60eb8f1

File tree

3 files changed

+70
-28
lines changed

3 files changed

+70
-28
lines changed

Diff for: src/com/cloudogu/ces/cesbuildlib/Trivy.groovy

+28-27
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class Trivy implements Serializable {
99
private String trivyImage
1010
private String trivyDirectory = "trivy"
1111

12-
Trivy(script, String trivyVersion = "0.57.1", String trivyImage = "aquasec/trivy", Docker docker = new Docker(script)) {
12+
Trivy(script, String trivyVersion = DEFAULT_TRIVY_VERSION, String trivyImage = DEFAULT_TRIVY_IMAGE, Docker docker = new Docker(script)) {
1313
this.script = script
1414
this.trivyVersion = trivyVersion
1515
this.trivyImage = trivyImage
@@ -94,39 +94,37 @@ class Trivy implements Serializable {
9494
) {
9595
String image = script.sh(script: "jq .Image ${doguDir}/dogu.json", returnStdout: true).trim()
9696
String version = script.sh(script: "jq .Version ${doguDir}/dogu.json", returnStdout: true).trim()
97-
return scanImage(image+":"+version, severityLevel, strategy, additionalFlags, trivyReportFile)
97+
return scanImage(image + ":" + version, severityLevel, strategy, additionalFlags, trivyReportFile)
9898
}
9999

100100
/**
101101
* Save the Trivy scan results as a file with a specific format
102102
*
103-
* @param format The format of the output file (@see TrivyScanFormat)
103+
* @param format The format of the output file {@link TrivyScanFormat}.
104+
* You may enter supported formats (sarif, cyclonedx, spdx, spdx-json, github, cosign-vuln, table or json)
105+
* or your own template ("template --template @FILENAME").
106+
* If you want to convert to a format that requires a list of packages, such as SBOM, you need to add
107+
* the `--list-all-pkgs` flag to the {@link Trivy#scanImage} call, when outputting in JSON
108+
* (See <a href="https://trivy.dev/latest/docs/configuration/reporting/?ref=anaisurl.com#converting">trivy docs</a>).
104109
* @param severity Severities of security issues to be added (taken from UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL)
105-
* @param formattedTrivyReportFilename The file name your report files should get, without file extension. E.g. "ubuntu24report"
110+
* @param formattedTrivyReportFilename The file name your report files should get, with file extension. E.g. "ubuntu24report.html"
106111
* @param trivyReportFile The "trivyReportFile" parameter you used in the "scanImage" function, if it was set
107112
*/
108-
void saveFormattedTrivyReport(String format = TrivyScanFormat.HTML, String severity = "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL", String formattedTrivyReportFilename = "formattedTrivyReport.txt", String trivyReportFile = "trivy/trivyReport.json") {
113+
void saveFormattedTrivyReport(String format = TrivyScanFormat.HTML,
114+
String severity = "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL",
115+
String formattedTrivyReportFilename = null,
116+
String trivyReportFile = "trivy/trivyReport.json") {
117+
118+
// set default report filename depending on the chosen format
119+
if (formattedTrivyReportFilename == null) {
120+
formattedTrivyReportFilename = "formattedTrivyReport" + getFileExtension(format)
121+
}
122+
109123
String formatString
110-
String defaultSeverityLevels = "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL"
111-
String defaultFilename = "formattedTrivyReport.txt"
112124
switch (format) {
125+
// TrivyScanFormat.JSON and TrivyScanFormat.TABLE are handled by the default case, too
113126
case TrivyScanFormat.HTML:
114127
formatString = "template --template \"@/contrib/html.tpl\""
115-
if (formattedTrivyReportFilename == defaultFilename) {
116-
formattedTrivyReportFilename == "formattedTrivyReport.html"
117-
}
118-
break
119-
case TrivyScanFormat.JSON:
120-
formatString = "json"
121-
if (formattedTrivyReportFilename == defaultFilename) {
122-
formattedTrivyReportFilename == "formattedTrivyReport.json"
123-
}
124-
break
125-
case TrivyScanFormat.TABLE:
126-
formatString = "table"
127-
if (formattedTrivyReportFilename == defaultFilename) {
128-
formattedTrivyReportFilename == "formattedTrivyReport.table"
129-
}
130128
break
131129
default:
132130
// You may enter supported formats (sarif, cyclonedx, spdx, spdx-json, github, cosign-vuln, table or json)
@@ -135,7 +133,7 @@ class Trivy implements Serializable {
135133
// Check if "format" is a custom template from a file
136134
boolean isTemplateFormat = format ==~ /^template --template @\S+$/
137135
// Check if "format" is one of the trivyFormats or a template
138-
if (trivyFormats.any { format.contains(it) } || isTemplateFormat) {
136+
if (trivyFormats.any { (format == it) } || isTemplateFormat) {
139137
formatString = format
140138
break
141139
} else {
@@ -144,15 +142,18 @@ class Trivy implements Serializable {
144142
}
145143
}
146144
// Validate severity input parameter to prevent injection of additional parameters
147-
if (severity != defaultSeverityLevels) {
148-
if (!severity.split(',').every { it.trim() in ["UNKNOWN", "LOW", "MEDIUM", "HIGH", "CRITICAL"] }) {
149-
script.error("The severity levels provided ($severity) do not match the applicable levels (UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL).")
150-
}
145+
if (!severity.split(',').every { it.trim() in ["UNKNOWN", "LOW", "MEDIUM", "HIGH", "CRITICAL"] }) {
146+
script.error("The severity levels provided ($severity) do not match the applicable levels (UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL).")
151147
}
148+
152149
docker.image("${trivyImage}:${trivyVersion}")
153150
.inside("-v ${script.env.WORKSPACE}/.trivy/.cache:/root/.cache/") {
154151
script.sh(script: "trivy convert --format ${formatString} --severity ${severity} --output ${trivyDirectory}/${formattedTrivyReportFilename} ${trivyReportFile}")
155152
}
156153
script.archiveArtifacts artifacts: "${trivyDirectory}/${formattedTrivyReportFilename}.*", allowEmptyArchive: true
157154
}
155+
156+
private static String getFileExtension(String format) {
157+
return TrivyScanFormat.isStandardScanFormat(format) ? "." + format : ".txt"
158+
}
158159
}

Diff for: src/com/cloudogu/ces/cesbuildlib/TrivyScanFormat.groovy

+4
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,8 @@ class TrivyScanFormat {
1818
* Output as table.
1919
*/
2020
static String TABLE = "table"
21+
22+
static boolean isStandardScanFormat(String format) {
23+
return format == HTML || format == JSON || format == TABLE
24+
}
2125
}

Diff for: test/com/cloudogu/ces/cesbuildlib/TrivyTest.groovy

+38-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ class TrivyTest extends GroovyTestCase {
4949
// emulate trivy call with local trivy installation and check that it has the same behavior
5050
Files.createDirectories(trivyDir)
5151
Process process = trivyExec.exec(Trivy.DEFAULT_TRIVY_VERSION, trivyArguments, workDir)
52-
if(process.waitFor(2, TimeUnit.MINUTES)) {
52+
if (process.waitFor(2, TimeUnit.MINUTES)) {
5353
assertEquals(expectedStatusCode, process.exitValue())
5454
} else {
5555
process.destroyForcibly()
@@ -120,4 +120,41 @@ class TrivyTest extends GroovyTestCase {
120120
}
121121
assertTrue(gotException)
122122
}
123+
124+
void testSaveFormattedTrivyReport() {
125+
ScriptMock scriptMock = mockSaveFormattedTrivyReport(
126+
"template --template \"@/contrib/html.tpl\"",
127+
"UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL",
128+
"trivy/formattedTrivyReport.html")
129+
130+
println(scriptMock.archivedArtifacts)
131+
assertFalse(true)
132+
}
133+
134+
ScriptMock mockSaveFormattedTrivyReport(String expectedFormat, String expectedSeverity, String expectedOutput) {
135+
String trivyArguments = "convert --format ${expectedFormat} --severity ${expectedSeverity} --output ${expectedOutput} trivy/trivyReport.json"
136+
String expectedTrivyCommand = "trivy $trivyArguments"
137+
138+
def scriptMock = new ScriptMock()
139+
scriptMock.env.WORKSPACE = "/test"
140+
Docker dockerMock = mock(Docker.class)
141+
Docker.Image imageMock = mock(Docker.Image.class)
142+
when(dockerMock.image(trivyImage)).thenReturn(imageMock)
143+
when(imageMock.inside(matches("-v /test/.trivy/.cache:/root/.cache/"), any())).thenAnswer(new Answer<Integer>() {
144+
@Override
145+
Integer answer(InvocationOnMock invocation) throws Throwable {
146+
// mock "sh trivy" so that it returns the expected status code and check trivy arguments
147+
Closure closure = invocation.getArgument(1)
148+
scriptMock.expectedShRetValueForScript.put(expectedTrivyCommand, 0)
149+
closure.call()
150+
assertEquals(expectedTrivyCommand, scriptMock.getActualShMapArgs().getLast())
151+
println(scriptMock.getActualShMapArgs().getLast())
152+
return 0
153+
}
154+
})
155+
Trivy trivy = new Trivy(scriptMock, Trivy.DEFAULT_TRIVY_VERSION, Trivy.DEFAULT_TRIVY_IMAGE, dockerMock)
156+
trivy.saveFormattedTrivyReport()
157+
158+
return scriptMock
159+
}
123160
}

0 commit comments

Comments
 (0)