10
10
import java .net .URISyntaxException ;
11
11
import java .net .URL ;
12
12
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 ;
13
17
import java .util .*;
18
+ import java .util .stream .Collectors ;
14
19
15
- import static org .graalvm .internal .tck .DockerUtils .extractImagesNames ;
16
- import static org .graalvm .internal .tck .DockerUtils .getAllAllowedImages ;
17
20
18
21
public abstract class GrypeTask extends DefaultTask {
19
22
@@ -33,88 +36,190 @@ void setNewCommit(String newCommit) {
33
36
private String newCommit ;
34
37
private String baseCommit ;
35
38
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" ;
37
41
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 ){}
44
43
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
+ }
52
48
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 ();
56
61
}
57
62
58
- return diffFiles ;
63
+ public void printVulnerabilityStatus () {
64
+ System .out .println ("Image: " + image + " contains " + vulnerabilities .critical () + " critical and " + vulnerabilities .high () + " high vulnerabilities" );
65
+ }
59
66
}
60
67
61
68
@ TaskAction
62
69
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 ();
67
73
} 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." );
69
88
}
89
+ }
70
90
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 ++;
93
119
}
94
120
}
121
+ }
95
122
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
+ });
99
146
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 ++;
102
156
}
103
157
}
104
158
105
159
inputStream .close ();
106
160
execOutput .close ();
161
+ } catch (IOException e ) {
162
+ throw new RuntimeException (e );
107
163
}
108
164
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
+ }
114
167
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." );
117
188
}
189
+
190
+ return DockerUtils .extractImagesNames (diffFiles );
118
191
}
119
192
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
+ }
120
225
}
0 commit comments