View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle;
21  
22  import java.io.ByteArrayOutputStream;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.OutputStream;
26  import java.io.OutputStreamWriter;
27  import java.io.PrintWriter;
28  import java.io.StringWriter;
29  import java.nio.charset.StandardCharsets;
30  import java.util.ArrayList;
31  import java.util.List;
32  import java.util.Locale;
33  
34  import com.puppycrawl.tools.checkstyle.api.AuditEvent;
35  import com.puppycrawl.tools.checkstyle.api.AuditListener;
36  import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
37  
38  /**
39   * Simple SARIF logger.
40   * SARIF stands for the static analysis results interchange format.
41   * See <a href="https://sarifweb.azurewebsites.net/">reference</a>
42   */
43  public class SarifLogger extends AbstractAutomaticBean implements AuditListener {
44  
45      /** The length of unicode placeholder. */
46      private static final int UNICODE_LENGTH = 4;
47  
48      /** Unicode escaping upper limit. */
49      private static final int UNICODE_ESCAPE_UPPER_LIMIT = 0x1F;
50  
51      /** Input stream buffer size. */
52      private static final int BUFFER_SIZE = 1024;
53  
54      /** The placeholder for message. */
55      private static final String MESSAGE_PLACEHOLDER = "${message}";
56  
57      /** The placeholder for severity level. */
58      private static final String SEVERITY_LEVEL_PLACEHOLDER = "${severityLevel}";
59  
60      /** The placeholder for uri. */
61      private static final String URI_PLACEHOLDER = "${uri}";
62  
63      /** The placeholder for line. */
64      private static final String LINE_PLACEHOLDER = "${line}";
65  
66      /** The placeholder for column. */
67      private static final String COLUMN_PLACEHOLDER = "${column}";
68  
69      /** The placeholder for rule id. */
70      private static final String RULE_ID_PLACEHOLDER = "${ruleId}";
71  
72      /** The placeholder for version. */
73      private static final String VERSION_PLACEHOLDER = "${version}";
74  
75      /** The placeholder for results. */
76      private static final String RESULTS_PLACEHOLDER = "${results}";
77  
78      /** Helper writer that allows easy encoding and printing. */
79      private final PrintWriter writer;
80  
81      /** Close output stream in auditFinished. */
82      private final boolean closeStream;
83  
84      /** The results. */
85      private final List<String> results = new ArrayList<>();
86  
87      /** Content for the entire report. */
88      private final String report;
89  
90      /** Content for result representing an error with source line and column. */
91      private final String resultLineColumn;
92  
93      /** Content for result representing an error with source line only. */
94      private final String resultLineOnly;
95  
96      /** Content for result representing an error with filename only and without source location. */
97      private final String resultFileOnly;
98  
99      /** Content for result representing an error without filename or location. */
100     private final String resultErrorOnly;
101 
102     /**
103      * Creates a new {@code SarifLogger} instance.
104      *
105      * @param outputStream where to log audit events
106      * @param outputStreamOptions if {@code CLOSE} that should be closed in auditFinished()
107      * @throws IllegalArgumentException if outputStreamOptions is null
108      * @throws IOException if there is reading errors.
109      */
110     public SarifLogger(
111         OutputStream outputStream,
112         OutputStreamOptions outputStreamOptions) throws IOException {
113         if (outputStreamOptions == null) {
114             throw new IllegalArgumentException("Parameter outputStreamOptions can not be null");
115         }
116         writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
117         closeStream = outputStreamOptions == OutputStreamOptions.CLOSE;
118         report = readResource("/com/puppycrawl/tools/checkstyle/sarif/SarifReport.template");
119         resultLineColumn =
120             readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultLineColumn.template");
121         resultLineOnly =
122             readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultLineOnly.template");
123         resultFileOnly =
124             readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultFileOnly.template");
125         resultErrorOnly =
126             readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultErrorOnly.template");
127     }
128 
129     @Override
130     protected void finishLocalSetup() {
131         // No code by default
132     }
133 
134     @Override
135     public void auditStarted(AuditEvent event) {
136         // No code by default
137     }
138 
139     @Override
140     public void auditFinished(AuditEvent event) {
141         final String version = SarifLogger.class.getPackage().getImplementationVersion();
142         final String rendered = report
143             .replace(VERSION_PLACEHOLDER, String.valueOf(version))
144             .replace(RESULTS_PLACEHOLDER, String.join(",\n", results));
145         writer.print(rendered);
146         if (closeStream) {
147             writer.close();
148         }
149         else {
150             writer.flush();
151         }
152     }
153 
154     @Override
155     public void addError(AuditEvent event) {
156         if (event.getColumn() > 0) {
157             results.add(resultLineColumn
158                 .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel()))
159                 .replace(URI_PLACEHOLDER, event.getFileName())
160                 .replace(COLUMN_PLACEHOLDER, Integer.toString(event.getColumn()))
161                 .replace(LINE_PLACEHOLDER, Integer.toString(event.getLine()))
162                 .replace(MESSAGE_PLACEHOLDER, escape(event.getMessage()))
163                 .replace(RULE_ID_PLACEHOLDER, event.getViolation().getKey())
164             );
165         }
166         else {
167             results.add(resultLineOnly
168                 .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel()))
169                 .replace(URI_PLACEHOLDER, event.getFileName())
170                 .replace(LINE_PLACEHOLDER, Integer.toString(event.getLine()))
171                 .replace(MESSAGE_PLACEHOLDER, escape(event.getMessage()))
172                 .replace(RULE_ID_PLACEHOLDER, event.getViolation().getKey())
173             );
174         }
175     }
176 
177     @Override
178     public void addException(AuditEvent event, Throwable throwable) {
179         final StringWriter stringWriter = new StringWriter();
180         final PrintWriter printer = new PrintWriter(stringWriter);
181         throwable.printStackTrace(printer);
182         if (event.getFileName() == null) {
183             results.add(resultErrorOnly
184                 .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel()))
185                 .replace(MESSAGE_PLACEHOLDER, escape(stringWriter.toString()))
186             );
187         }
188         else {
189             results.add(resultFileOnly
190                 .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel()))
191                 .replace(URI_PLACEHOLDER, event.getFileName())
192                 .replace(MESSAGE_PLACEHOLDER, escape(stringWriter.toString()))
193             );
194         }
195     }
196 
197     @Override
198     public void fileStarted(AuditEvent event) {
199         // No need to implement this method in this class
200     }
201 
202     @Override
203     public void fileFinished(AuditEvent event) {
204         // No need to implement this method in this class
205     }
206 
207     /**
208      * Render the severity level into SARIF severity level.
209      *
210      * @param severityLevel the Severity level.
211      * @return the rendered severity level in string.
212      */
213     private static String renderSeverityLevel(SeverityLevel severityLevel) {
214         final String renderedSeverityLevel;
215         switch (severityLevel) {
216             case IGNORE:
217                 renderedSeverityLevel = "none";
218                 break;
219             case INFO:
220                 renderedSeverityLevel = "note";
221                 break;
222             case WARNING:
223                 renderedSeverityLevel = "warning";
224                 break;
225             case ERROR:
226             default:
227                 renderedSeverityLevel = "error";
228                 break;
229         }
230         return renderedSeverityLevel;
231     }
232 
233     /**
234      * Escape \b, \f, \n, \r, \t, \", \\ and U+0000 through U+001F.
235      * See <a href="https://www.ietf.org/rfc/rfc4627.txt">reference</a> - 2.5. Strings
236      *
237      * @param value the value to escape.
238      * @return the escaped value if necessary.
239      */
240     public static String escape(String value) {
241         final int length = value.length();
242         final StringBuilder sb = new StringBuilder(length);
243         for (int i = 0; i < length; i++) {
244             final char chr = value.charAt(i);
245             switch (chr) {
246                 case '"':
247                     sb.append("\\\"");
248                     break;
249                 case '\\':
250                     sb.append("\\\\");
251                     break;
252                 case '\b':
253                     sb.append("\\b");
254                     break;
255                 case '\f':
256                     sb.append("\\f");
257                     break;
258                 case '\n':
259                     sb.append("\\n");
260                     break;
261                 case '\r':
262                     sb.append("\\r");
263                     break;
264                 case '\t':
265                     sb.append("\\t");
266                     break;
267                 case '/':
268                     sb.append("\\/");
269                     break;
270                 default:
271                     if (chr <= UNICODE_ESCAPE_UPPER_LIMIT) {
272                         sb.append(escapeUnicode1F(chr));
273                     }
274                     else {
275                         sb.append(chr);
276                     }
277                     break;
278             }
279         }
280         return sb.toString();
281     }
282 
283     /**
284      * Escape the character between 0x00 to 0x1F in JSON.
285      *
286      * @param chr the character to be escaped.
287      * @return the escaped string.
288      */
289     private static String escapeUnicode1F(char chr) {
290         final String hexString = Integer.toHexString(chr);
291         return "\\u"
292                 + "0".repeat(UNICODE_LENGTH - hexString.length())
293                 + hexString.toUpperCase(Locale.US);
294     }
295 
296     /**
297      * Read string from given resource.
298      *
299      * @param name name of the desired resource
300      * @return the string content from the give resource
301      * @throws IOException if there is reading errors
302      */
303     public static String readResource(String name) throws IOException {
304         try (InputStream inputStream = SarifLogger.class.getResourceAsStream(name);
305              ByteArrayOutputStream result = new ByteArrayOutputStream()) {
306             if (inputStream == null) {
307                 throw new IOException("Cannot find the resource " + name);
308             }
309             final byte[] buffer = new byte[BUFFER_SIZE];
310             int length = inputStream.read(buffer);
311             while (length != -1) {
312                 result.write(buffer, 0, length);
313                 length = inputStream.read(buffer);
314             }
315             return result.toString(StandardCharsets.UTF_8);
316         }
317     }
318 }