11package 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 ;
311import org .gradle .api .DefaultTask ;
412import org .gradle .api .tasks .TaskAction ;
513import org .gradle .api .tasks .options .Option ;
1018import java .net .URISyntaxException ;
1119import java .net .URL ;
1220import 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 ;
1325import 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
1829public 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}
0 commit comments