Skip to content

Commit 368d448

Browse files
committed
Make vulnerabilities scanner less strict
1 parent 124d15d commit 368d448

File tree

3 files changed

+236
-63
lines changed

3 files changed

+236
-63
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
name: Orchestrate Batches
2+
3+
on:
4+
workflow_dispatch:
5+
6+
permissions:
7+
contents: write
8+
actions: write
9+
10+
jobs:
11+
calculate-matrix:
12+
name: "📋 Get list of all supported libraries with newer versions"
13+
permissions: write-all
14+
runs-on: "ubuntu-22.04"
15+
timeout-minutes: 5
16+
outputs:
17+
matrix: ${{ steps.set-matrix.outputs.matrix }}
18+
steps:
19+
- name: "☁️ Checkout repository"
20+
uses: actions/checkout@v4
21+
- name: "🔧 Prepare environment"
22+
uses: graalvm/setup-graalvm@v1
23+
with:
24+
java-version: '21'
25+
distribution: 'graalvm'
26+
github-token: ${{ secrets.GITHUB_TOKEN }}
27+
- name: "🕸️ Populate matrix"
28+
id: set-matrix
29+
run: |
30+
./gradlew fetchExistingLibrariesWithNewerVersions --matrixLimit=200
31+
32+
orchestrate:
33+
runs-on: ubuntu-latest
34+
env:
35+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36+
needs: get-all-libraries
37+
strategy:
38+
fail-fast: false
39+
matrix: ${{ fromJson(needs.calculate-matrix.outputs.matrix) }}
40+
steps:
41+
- name: Calculate and Dispatch Batches
42+
run: |
43+
44+
TOTAL=2000
45+
BATCH_SIZE=250
46+
for START in $(seq 0 $BATCH_SIZE $((TOTAL - 1))); do
47+
END=$((START + BATCH_SIZE - 1))
48+
if (( END >= TOTAL )); then END=$((TOTAL - 1)); fi
49+
50+
echo "Dispatching batch: $START to $END"
51+
52+
gh workflow run run-batch.yml \
53+
--ref main \
54+
-f batch_start=$START \
55+
-f batch_end=$END
56+
57+
# Optional: sleep if you want to throttle how quickly batches start
58+
# sleep 10
59+
done
60+
env:
61+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
62+
# Install the GitHub CLI (gh)
63+
- name: Install GitHub CLI
64+
run: |
65+
sudo apt-get update
66+
sudo apt-get install gh

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
}

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

Lines changed: 164 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@
1010
import java.net.URISyntaxException;
1111
import java.net.URL;
1212
import java.nio.charset.StandardCharsets;
13+
import java.nio.file.FileSystem;
14+
import java.nio.file.FileSystems;
15+
import java.nio.file.Files;
16+
import java.nio.file.Path;
1317
import java.util.*;
18+
import java.util.stream.Collectors;
1419

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

1821
public abstract class GrypeTask extends DefaultTask {
1922

@@ -33,88 +36,190 @@ void setNewCommit(String newCommit) {
3336
private String newCommit;
3437
private String baseCommit;
3538

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

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-
});
42+
private record Vulnerabilities(int critical, int high){}
4443

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();
44+
private record DockerImage(String image, Vulnerabilities vulnerabilities) {
45+
public String getImageName() {
46+
return DockerUtils.getImageName(image);
47+
}
5248

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.");
49+
public boolean isVulnerableImage() {
50+
return vulnerabilities.critical() > 0 || vulnerabilities.high() > 0;
51+
}
52+
53+
public boolean isLessVulnerable(DockerImage other) {
54+
// first check number of critical vulnerabilities
55+
if (this.vulnerabilities.critical() < other.vulnerabilities().critical()) {
56+
return true;
57+
}
58+
59+
// if number of critical vulnerabilities is the same => check number of high vulnerabilities
60+
return this.vulnerabilities.critical() == other.vulnerabilities().critical() && this.vulnerabilities.high() < other.vulnerabilities().high();
5661
}
5762

58-
return diffFiles;
63+
public void printVulnerabilityStatus() {
64+
System.out.println("Image: " + image + " contains " + vulnerabilities.critical() + " critical and " + vulnerabilities.high() + " high vulnerabilities");
65+
}
5966
}
6067

6168
@TaskAction
6269
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();
70+
boolean scanAllAllowedImages = baseCommit == null && newCommit == null;
71+
if (scanAllAllowedImages) {
72+
scanAllImages();
6773
} else {
68-
allowedImages = extractImagesNames(getChangedImages(baseCommit, newCommit));
74+
scanChangedImages();
75+
}
76+
}
77+
78+
/**
79+
* Re-scans all images from allowed images list
80+
*/
81+
private void scanAllImages() {
82+
Set<DockerImage> imagesToCheck = DockerUtils.getAllAllowedImages().stream().map(this::makeDockerImage).collect(Collectors.toSet());
83+
List<DockerImage> vulnerableImages = imagesToCheck.stream().filter(DockerImage::isVulnerableImage).toList();
84+
85+
if (!vulnerableImages.isEmpty()) {
86+
vulnerableImages.forEach(DockerImage::printVulnerabilityStatus);
87+
throw new IllegalStateException("Highly vulnerable images found. Please check the list of vulnerable images provided above.");
6988
}
89+
}
7090

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++;
91+
/**
92+
* Scans images that have been changed between org.graalvm.internal.tck.GrypeTask#baseCommit and org.graalvm.internal.tck.GrypeTask#newCommit.
93+
* If changed images are less vulnerable than previously allowed images, they won't be reported as vulnerable
94+
*/
95+
private void scanChangedImages() throws IOException, URISyntaxException {
96+
Set<DockerImage> imagesToCheck = getChangedImages().stream().map(this::makeDockerImage).collect(Collectors.toSet());
97+
List<DockerImage> vulnerableImages = imagesToCheck.stream().filter(DockerImage::isVulnerableImage).toList();
98+
99+
if (!vulnerableImages.isEmpty()) {
100+
int acceptedImages = 0;
101+
Set<String> currentlyAllowedImages = getAllowedImagesFromMaster();
102+
103+
for (DockerImage image : vulnerableImages) {
104+
image.printVulnerabilityStatus();
105+
106+
// get allowed image with the same name, if it exists
107+
Optional<String> existingAllowedImage = currentlyAllowedImages.stream()
108+
.filter(allowedImage -> DockerUtils.getImageName(allowedImage).equalsIgnoreCase(image.getImageName()))
109+
.findFirst();
110+
111+
// check if a new image is less vulnerable than the existing one
112+
if (existingAllowedImage.isPresent()) {
113+
DockerImage imageToCompare = makeDockerImage(existingAllowedImage.get());
114+
imageToCompare.printVulnerabilityStatus();
115+
116+
if (image.isLessVulnerable(imageToCompare)) {
117+
System.out.println("Accepting: " + image.image() + " because it has less vulnerabilities than existing: " + imageToCompare.image());
118+
acceptedImages++;
93119
}
94120
}
121+
}
95122

96-
if (numberOfHigh > 0 || numberOfCritical > 0) {
97-
vulnerableImages.add("Image: " + image + " contains " + numberOfCritical + " critical, and " + numberOfHigh + " high vulnerabilities");
98-
}
123+
if (acceptedImages < vulnerableImages.size()) {
124+
throw new IllegalStateException("Highly vulnerable images found. Please check the list of vulnerable images provided above.");
125+
}
126+
}
127+
}
128+
129+
private DockerImage makeDockerImage(String image) {
130+
System.out.println("Generating info for docker image: " + image);
131+
return new DockerImage(image, getVulnerabilities(image));
132+
}
133+
134+
private Vulnerabilities getVulnerabilities(String image) {
135+
int numberOfHigh = 0;
136+
int numberOfCritical = 0;
137+
String[] command = {"-c", "grype -o json " + image + JQ_MATCHER};
138+
139+
// call Grype to get vulnerabilities
140+
ByteArrayOutputStream execOutput = new ByteArrayOutputStream();
141+
getExecOperations().exec(execSpec -> {
142+
execSpec.setExecutable("/bin/sh");
143+
execSpec.setArgs(List.of(command));
144+
execSpec.setStandardOutput(execOutput);
145+
});
99146

100-
if (numberOfHigh > 4 || numberOfCritical > 0) {
101-
shouldFail = true;
147+
// parse Grype output
148+
ByteArrayInputStream inputStream = new ByteArrayInputStream(execOutput.toByteArray());
149+
try (BufferedReader stdOut = new BufferedReader(new InputStreamReader(inputStream))) {
150+
String line;
151+
while ((line = stdOut.readLine()) != null) {
152+
if (line.contains("\"severity\":\"High\"")) {
153+
numberOfHigh++;
154+
} else if (line.contains("\"severity\":\"Critical\"")) {
155+
numberOfCritical++;
102156
}
103157
}
104158

105159
inputStream.close();
106160
execOutput.close();
161+
} catch (IOException e) {
162+
throw new RuntimeException(e);
107163
}
108164

109-
if (!vulnerableImages.isEmpty()) {
110-
System.err.println("Vulnerable images found:");
111-
System.err.println("===========================================================");
112-
vulnerableImages.forEach(System.err::println);
113-
}
165+
return new Vulnerabilities(numberOfCritical, numberOfHigh);
166+
}
114167

115-
if (shouldFail) {
116-
throw new IllegalStateException("Highly vulnerable images found. Please check the list of vulnerable images provided above.");
168+
/**
169+
* Get all docker images introduced between two commits
170+
*/
171+
private Set<String> getChangedImages() throws IOException, URISyntaxException {
172+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
173+
getExecOperations().exec(spec -> {
174+
spec.setStandardOutput(baos);
175+
spec.commandLine("git", "diff", "--name-only", "--diff-filter=ACMRT", baseCommit, newCommit);
176+
});
177+
178+
String output = baos.toString(StandardCharsets.UTF_8);
179+
List<URL> diffFiles = Arrays.stream(output.split("\\r?\\n"))
180+
.filter(path -> path.contains(DOCKERFILE_DIRECTORY))
181+
.map(path -> path.substring(path.lastIndexOf("/") + 1))
182+
.map(DockerUtils::getDockerFile)
183+
.toList();
184+
185+
if (diffFiles.isEmpty()) {
186+
throw new RuntimeException("There are no changed or new docker image founded. " +
187+
"This task should be executed only if there are changes in allowed-docker-images directory.");
117188
}
189+
190+
return DockerUtils.extractImagesNames(diffFiles);
118191
}
119192

193+
/**
194+
* Return all allowed docker images from master branch
195+
*/
196+
private Set<String> getAllowedImagesFromMaster() throws URISyntaxException, IOException {
197+
URL url = GrypeTask.class.getResource(DockerUtils.ALLOWED_DOCKER_IMAGES);
198+
if (url == null) {
199+
throw new RuntimeException("Cannot find allowed-docker-images directory");
200+
}
201+
202+
Set<String> allowedImages = new HashSet<>();
203+
try (FileSystem fs = FileSystems.newFileSystem(url.toURI(), Collections.emptyMap())) {
204+
List<String> files = Files.walk(fs.getPath(DockerUtils.ALLOWED_DOCKER_IMAGES))
205+
.filter(Files::isRegularFile)
206+
.map(Path::toString)
207+
.map(path -> path.substring(path.lastIndexOf("/") + 1))
208+
.map(DockerUtils::getDockerFile)
209+
.map(DockerUtils::fileNameFromJar)
210+
.toList();
211+
212+
for (String file : files) {
213+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
214+
getExecOperations().exec(spec -> {
215+
spec.setStandardOutput(baos);
216+
spec.commandLine("git", "show", "master:tests/tck-build-logic/src/main/resources" + file);
217+
});
218+
219+
allowedImages.add(baos.toString());
220+
}
221+
}
222+
223+
return allowedImages.stream().map(line -> line.substring("FROM".length()).trim()).collect(Collectors.toSet());
224+
}
120225
}

0 commit comments

Comments
 (0)