1
1
package org .graalvm .internal .tck ;
2
2
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 ;
3
11
import org .gradle .api .DefaultTask ;
4
12
import org .gradle .api .tasks .TaskAction ;
5
13
import org .gradle .api .tasks .options .Option ;
10
18
import java .net .URISyntaxException ;
11
19
import java .net .URL ;
12
20
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 ;
13
25
import java .util .*;
26
+ import java .util .stream .Collectors ;
14
27
15
- import static org .graalvm .internal .tck .DockerUtils .extractImagesNames ;
16
- import static org .graalvm .internal .tck .DockerUtils .getAllAllowedImages ;
17
28
18
29
public abstract class GrypeTask extends DefaultTask {
19
30
@@ -33,88 +44,198 @@ void setNewCommit(String newCommit) {
33
44
private String newCommit ;
34
45
private String baseCommit ;
35
46
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" ;
37
49
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" ;
44
52
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 ){}
52
54
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 ();
56
66
}
57
67
58
- return diffFiles ;
68
+ public void printVulnerabilityStatus () {
69
+ System .out .println ("Image: " + image + " contains " + vulnerabilities .critical () + " critical and " + vulnerabilities .high () + " high vulnerabilities" );
70
+ }
59
71
}
60
72
61
73
@ TaskAction
62
74
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 ();
67
78
} else {
68
- allowedImages = extractImagesNames ( getChangedImages ( baseCommit , newCommit ) );
79
+ scanChangedImages ( );
69
80
}
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 ();
70
89
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 ++;
93
124
}
94
125
}
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
+ }
95
165
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 ;
98
168
}
99
169
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 ++;
102
177
}
103
178
}
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
+ });
104
193
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." );
107
204
}
108
205
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" );
113
216
}
114
217
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
+ }
117
237
}
118
- }
119
238
239
+ return allowedImages .stream ().map (line -> line .substring ("FROM" .length ()).trim ()).collect (Collectors .toSet ());
240
+ }
120
241
}
0 commit comments