Skip to content

Commit e52f7e4

Browse files
authored
Make vulnerabilities scanner less strict (#669)
* Make vulnerabilities scanner less strict * Only accept images with less critical and less high vulnerabilities * Parse grype output as json
1 parent bd387e7 commit e52f7e4

File tree

3 files changed

+254
-64
lines changed

3 files changed

+254
-64
lines changed

tests/tck-build-logic/src/main/java/org/graalvm/internal/tck/DockerUtils.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package org.graalvm.internal.tck;
22

33
import java.io.BufferedReader;
4-
import java.io.File;
54
import java.io.IOException;
65
import java.io.InputStreamReader;
76
import java.net.MalformedURLException;
@@ -10,10 +9,9 @@
109
import java.net.URL;
1110
import java.nio.file.*;
1211
import java.util.*;
13-
import java.util.stream.Collectors;
1412

1513
public class DockerUtils {
16-
private static final String ALLOWED_DOCKER_IMAGES = "/allowed-docker-images";
14+
public static final String ALLOWED_DOCKER_IMAGES = "/allowed-docker-images";
1715

1816
private static URL getDockerfileDirectory() {
1917
URL url = DockerUtils.class.getResource(ALLOWED_DOCKER_IMAGES);
@@ -47,7 +45,7 @@ private static String imageNameFromFile(URL dockerFile) throws IOException, URIS
4745
return images.get(0);
4846
}
4947

50-
private static String fileNameFromJar(URL jarFile) {
48+
public static String fileNameFromJar(URL jarFile) {
5149
return jarFile.toString().split("!")[1];
5250
}
5351

@@ -78,4 +76,8 @@ public static Set<String> getAllAllowedImages() {
7876
}
7977
}
8078

79+
public static String getImageName(String imageWithVersion) {
80+
return imageWithVersion.split(":")[0];
81+
}
82+
8183
}
Lines changed: 181 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
package org.graalvm.internal.tck;
22

3+
import com.fasterxml.jackson.annotation.JsonInclude;
4+
import com.fasterxml.jackson.core.JsonFactory;
5+
import com.fasterxml.jackson.core.JsonParser;
6+
import com.fasterxml.jackson.core.type.TypeReference;
7+
import com.fasterxml.jackson.databind.ObjectMapper;
8+
import com.fasterxml.jackson.databind.SerializationFeature;
9+
import org.graalvm.internal.tck.model.MetadataIndexEntry;
10+
import org.graalvm.internal.tck.model.grype.GrypeEntry;
311
import org.gradle.api.DefaultTask;
412
import org.gradle.api.tasks.TaskAction;
513
import org.gradle.api.tasks.options.Option;
@@ -10,10 +18,13 @@
1018
import java.net.URISyntaxException;
1119
import java.net.URL;
1220
import java.nio.charset.StandardCharsets;
21+
import java.nio.file.FileSystem;
22+
import java.nio.file.FileSystems;
23+
import java.nio.file.Files;
24+
import java.nio.file.Path;
1325
import java.util.*;
26+
import java.util.stream.Collectors;
1427

15-
import static org.graalvm.internal.tck.DockerUtils.extractImagesNames;
16-
import static org.graalvm.internal.tck.DockerUtils.getAllAllowedImages;
1728

1829
public abstract class GrypeTask extends DefaultTask {
1930

@@ -33,88 +44,198 @@ void setNewCommit(String newCommit) {
3344
private String newCommit;
3445
private String baseCommit;
3546

36-
private final String jqMatcher = " | jq -c '.matches | .[] | .vulnerability | select(.severity | (contains(\"High\") or contains(\"Critical\")))'";
47+
private static final String JQ_MATCHER = " | jq -c '.matches | .[] | .vulnerability | select(.severity | (contains(\"High\") or contains(\"Critical\")))'";
48+
private static final String DOCKERFILE_DIRECTORY = "allowed-docker-images";
3749

38-
private List<URL> getChangedImages(String base, String head){
39-
ByteArrayOutputStream baos = new ByteArrayOutputStream();
40-
getExecOperations().exec(spec -> {
41-
spec.setStandardOutput(baos);
42-
spec.commandLine("git", "diff", "--name-only", "--diff-filter=ACMRT", base, head);
43-
});
50+
private static final String HIGH_VULNERABILITY = "HIGH";
51+
private static final String CRITICAL_VULNERABILITY = "CRITICAL";
4452

45-
String output = baos.toString(StandardCharsets.UTF_8);
46-
String dockerfileDirectory = "allowed-docker-images";
47-
List<URL> diffFiles = Arrays.stream(output.split("\\r?\\n"))
48-
.filter(path -> path.contains(dockerfileDirectory))
49-
.map(path -> path.substring(path.lastIndexOf("/") + 1))
50-
.map(DockerUtils::getDockerFile)
51-
.toList();
53+
private record Vulnerabilities(int critical, int high){}
5254

53-
if (diffFiles.isEmpty()) {
54-
throw new RuntimeException("There are no changed or new docker image founded. " +
55-
"This task should be executed only if there are changes in allowed-docker-images directory.");
55+
private record DockerImage(String image, Vulnerabilities vulnerabilities) {
56+
public String getImageName() {
57+
return DockerUtils.getImageName(image);
58+
}
59+
60+
public boolean isVulnerableImage() {
61+
return vulnerabilities.critical() > 0 || vulnerabilities.high() > 0;
62+
}
63+
64+
public boolean isLessVulnerable(DockerImage other) {
65+
return this.vulnerabilities.critical() < other.vulnerabilities().critical() && this.vulnerabilities.high() < other.vulnerabilities().high();
5666
}
5767

58-
return diffFiles;
68+
public void printVulnerabilityStatus() {
69+
System.out.println("Image: " + image + " contains " + vulnerabilities.critical() + " critical and " + vulnerabilities.high() + " high vulnerabilities");
70+
}
5971
}
6072

6173
@TaskAction
6274
void run() throws IllegalStateException, IOException, URISyntaxException {
63-
List<String> vulnerableImages = new ArrayList<>();
64-
Set<String> allowedImages;
65-
if (baseCommit == null && newCommit == null) {
66-
allowedImages = getAllAllowedImages();
75+
boolean scanAllAllowedImages = baseCommit == null && newCommit == null;
76+
if (scanAllAllowedImages) {
77+
scanAllImages();
6778
} else {
68-
allowedImages = extractImagesNames(getChangedImages(baseCommit, newCommit));
79+
scanChangedImages();
6980
}
81+
}
82+
83+
/**
84+
* Re-scans all images from allowed images list
85+
*/
86+
private void scanAllImages() {
87+
Set<DockerImage> imagesToCheck = DockerUtils.getAllAllowedImages().stream().map(this::makeDockerImage).collect(Collectors.toSet());
88+
List<DockerImage> vulnerableImages = imagesToCheck.stream().filter(DockerImage::isVulnerableImage).toList();
7089

71-
boolean shouldFail = false;
72-
for (String image : allowedImages) {
73-
System.out.println("Checking image: " + image);
74-
String[] command = { "-c", "grype -o json " + image + jqMatcher };
75-
76-
ByteArrayOutputStream execOutput = new ByteArrayOutputStream();
77-
getExecOperations().exec(execSpec -> {
78-
execSpec.setExecutable("/bin/sh");
79-
execSpec.setArgs(List.of(command));
80-
execSpec.setStandardOutput(execOutput);
81-
});
82-
83-
ByteArrayInputStream inputStream = new ByteArrayInputStream(execOutput.toByteArray());
84-
try (BufferedReader stdOut = new BufferedReader(new InputStreamReader(inputStream))) {
85-
int numberOfHigh = 0;
86-
int numberOfCritical = 0;
87-
String line;
88-
while ((line = stdOut.readLine()) != null) {
89-
if (line.contains("\"severity\":\"High\"")) {
90-
numberOfHigh++;
91-
}else if (line.contains("\"severity\":\"Critical\"")) {
92-
numberOfCritical++;
90+
if (!vulnerableImages.isEmpty()) {
91+
vulnerableImages.forEach(DockerImage::printVulnerabilityStatus);
92+
throw new IllegalStateException("Highly vulnerable images found. Please check the list of vulnerable images provided above.");
93+
}
94+
}
95+
96+
/**
97+
* Scans images that have been changed between org.graalvm.internal.tck.GrypeTask#baseCommit and org.graalvm.internal.tck.GrypeTask#newCommit.
98+
* If changed images are less vulnerable than previously allowed images, they won't be reported as vulnerable
99+
*/
100+
private void scanChangedImages() throws IOException, URISyntaxException {
101+
Set<DockerImage> imagesToCheck = getChangedImages().stream().map(this::makeDockerImage).collect(Collectors.toSet());
102+
List<DockerImage> vulnerableImages = imagesToCheck.stream().filter(DockerImage::isVulnerableImage).toList();
103+
104+
if (!vulnerableImages.isEmpty()) {
105+
int acceptedImages = 0;
106+
Set<String> currentlyAllowedImages = getAllowedImagesFromMaster();
107+
108+
for (DockerImage image : vulnerableImages) {
109+
image.printVulnerabilityStatus();
110+
111+
// get allowed image with the same name, if it exists
112+
Optional<String> existingAllowedImage = currentlyAllowedImages.stream()
113+
.filter(allowedImage -> DockerUtils.getImageName(allowedImage).equalsIgnoreCase(image.getImageName()))
114+
.findFirst();
115+
116+
// check if a new image is less vulnerable than the existing one
117+
if (existingAllowedImage.isPresent()) {
118+
DockerImage imageToCompare = makeDockerImage(existingAllowedImage.get());
119+
imageToCompare.printVulnerabilityStatus();
120+
121+
if (image.isLessVulnerable(imageToCompare)) {
122+
System.out.println("Accepting: " + image.image() + " because it has less vulnerabilities than existing: " + imageToCompare.image());
123+
acceptedImages++;
93124
}
94125
}
126+
}
127+
128+
if (acceptedImages < vulnerableImages.size()) {
129+
throw new IllegalStateException("Highly vulnerable images found. Please check the list of vulnerable images provided above.");
130+
}
131+
}
132+
}
133+
134+
private DockerImage makeDockerImage(String image) {
135+
System.out.println("Generating info for docker image: " + image);
136+
try {
137+
return new DockerImage(image, getVulnerabilities(image));
138+
} catch (IOException e) {
139+
throw new RuntimeException("Cannot parse grype output for image: " + image + " .Reason: " + e.getMessage());
140+
}
141+
}
142+
143+
private Vulnerabilities getVulnerabilities(String image) throws IOException {
144+
int numberOfHigh = 0;
145+
int numberOfCritical = 0;
146+
String[] command = {"-c", "grype -o json " + image + JQ_MATCHER};
147+
148+
// call Grype to get vulnerabilities
149+
ByteArrayOutputStream execOutput = new ByteArrayOutputStream();
150+
getExecOperations().exec(execSpec -> {
151+
execSpec.setExecutable("/bin/sh");
152+
execSpec.setArgs(List.of(command));
153+
execSpec.setStandardOutput(execOutput);
154+
});
155+
156+
// parse Grype output
157+
ByteArrayInputStream inputStream = new ByteArrayInputStream(execOutput.toByteArray());
158+
ObjectMapper mapper = new ObjectMapper();
159+
JsonFactory factory = mapper.getFactory();
160+
try (JsonParser parser = factory.createParser(inputStream)) {
161+
while (!parser.isClosed()) {
162+
if (parser.nextToken() == null) {
163+
break;
164+
}
95165

96-
if (numberOfHigh > 0 || numberOfCritical > 0) {
97-
vulnerableImages.add("Image: " + image + " contains " + numberOfCritical + " critical, and " + numberOfHigh + " high vulnerabilities");
166+
if (parser.currentToken() != com.fasterxml.jackson.core.JsonToken.START_OBJECT) {
167+
continue;
98168
}
99169

100-
if (numberOfHigh > 4 || numberOfCritical > 0) {
101-
shouldFail = true;
170+
GrypeEntry vuln = mapper.readValue(parser, GrypeEntry.class);
171+
if (vuln.severity.equalsIgnoreCase(CRITICAL_VULNERABILITY)) {
172+
numberOfCritical++;
173+
}
174+
175+
if (vuln.severity.equalsIgnoreCase(HIGH_VULNERABILITY)) {
176+
numberOfHigh++;
102177
}
103178
}
179+
}
180+
181+
return new Vulnerabilities(numberOfCritical, numberOfHigh);
182+
}
183+
184+
/**
185+
* Get all docker images introduced between two commits
186+
*/
187+
private Set<String> getChangedImages() throws IOException, URISyntaxException {
188+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
189+
getExecOperations().exec(spec -> {
190+
spec.setStandardOutput(baos);
191+
spec.commandLine("git", "diff", "--name-only", "--diff-filter=ACMRT", baseCommit, newCommit);
192+
});
104193

105-
inputStream.close();
106-
execOutput.close();
194+
String output = baos.toString(StandardCharsets.UTF_8);
195+
List<URL> diffFiles = Arrays.stream(output.split("\\r?\\n"))
196+
.filter(path -> path.contains(DOCKERFILE_DIRECTORY))
197+
.map(path -> path.substring(path.lastIndexOf("/") + 1))
198+
.map(DockerUtils::getDockerFile)
199+
.toList();
200+
201+
if (diffFiles.isEmpty()) {
202+
throw new RuntimeException("There are no changed or new docker image founded. " +
203+
"This task should be executed only if there are changes in allowed-docker-images directory.");
107204
}
108205

109-
if (!vulnerableImages.isEmpty()) {
110-
System.err.println("Vulnerable images found:");
111-
System.err.println("===========================================================");
112-
vulnerableImages.forEach(System.err::println);
206+
return DockerUtils.extractImagesNames(diffFiles);
207+
}
208+
209+
/**
210+
* Return all allowed docker images from master branch
211+
*/
212+
private Set<String> getAllowedImagesFromMaster() throws URISyntaxException, IOException {
213+
URL url = GrypeTask.class.getResource(DockerUtils.ALLOWED_DOCKER_IMAGES);
214+
if (url == null) {
215+
throw new RuntimeException("Cannot find allowed-docker-images directory");
113216
}
114217

115-
if (shouldFail) {
116-
throw new IllegalStateException("Highly vulnerable images found. Please check the list of vulnerable images provided above.");
218+
Set<String> allowedImages = new HashSet<>();
219+
try (FileSystem fs = FileSystems.newFileSystem(url.toURI(), Collections.emptyMap())) {
220+
List<String> files = Files.walk(fs.getPath(DockerUtils.ALLOWED_DOCKER_IMAGES))
221+
.filter(Files::isRegularFile)
222+
.map(Path::toString)
223+
.map(path -> path.substring(path.lastIndexOf("/") + 1))
224+
.map(DockerUtils::getDockerFile)
225+
.map(DockerUtils::fileNameFromJar)
226+
.toList();
227+
228+
for (String file : files) {
229+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
230+
getExecOperations().exec(spec -> {
231+
spec.setStandardOutput(baos);
232+
spec.commandLine("git", "show", "master:tests/tck-build-logic/src/main/resources" + file);
233+
});
234+
235+
allowedImages.add(baos.toString());
236+
}
117237
}
118-
}
119238

239+
return allowedImages.stream().map(line -> line.substring("FROM".length()).trim()).collect(Collectors.toSet());
240+
}
120241
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package org.graalvm.internal.tck.model.grype;
2+
3+
/*
4+
* JSON model for metadata/index.json.
5+
*/
6+
import java.util.List;
7+
import java.util.Map;
8+
9+
public class GrypeEntry {
10+
public String id;
11+
public String dataSource;
12+
public String namespace;
13+
public String severity;
14+
public List<String> urls;
15+
public String description;
16+
public List<Cvss> cvss;
17+
public List<KnownExploited> knownExploited;
18+
public List<Epss> epss;
19+
public Fix fix;
20+
public List<Object> advisories; // unknown structure
21+
public Double risk;
22+
}
23+
24+
class Cvss {
25+
public String source;
26+
public String type;
27+
public String version;
28+
public String vector;
29+
public Metrics metrics;
30+
public Map<String, Object> vendorMetadata;
31+
}
32+
class Metrics {
33+
public Double baseScore;
34+
public Double exploitabilityScore;
35+
public Double impactScore;
36+
}
37+
38+
class KnownExploited {
39+
public String cve;
40+
public String vendorProject;
41+
public String product;
42+
public String dateAdded;
43+
public String requiredAction;
44+
public String dueDate;
45+
public String knownRansomwareCampaignUse;
46+
public String notes;
47+
public List<String> urls;
48+
public List<String> cwes;
49+
}
50+
51+
class Epss {
52+
public String cve;
53+
public Double epss;
54+
public Double percentile;
55+
public String date;
56+
}
57+
58+
class Fix {
59+
public List<String> versions;
60+
public String state;
61+
public List<AvailableFix> available;
62+
}
63+
class AvailableFix {
64+
public String version;
65+
public String date;
66+
public String kind;
67+
}

0 commit comments

Comments
 (0)