001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2022 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018//////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle; 021 022import java.io.ByteArrayOutputStream; 023import java.io.IOException; 024import java.io.InputStream; 025import java.io.OutputStream; 026import java.io.OutputStreamWriter; 027import java.io.PrintWriter; 028import java.io.StringWriter; 029import java.nio.charset.StandardCharsets; 030import java.util.ArrayList; 031import java.util.List; 032import java.util.Locale; 033 034import com.puppycrawl.tools.checkstyle.api.AuditEvent; 035import com.puppycrawl.tools.checkstyle.api.AuditListener; 036import com.puppycrawl.tools.checkstyle.api.AutomaticBean; 037import com.puppycrawl.tools.checkstyle.api.SeverityLevel; 038 039/** 040 * Simple SARIF logger. 041 * SARIF stands for the static analysis results interchange format. 042 * Reference: https://sarifweb.azurewebsites.net/ 043 */ 044public class SarifLogger extends AutomaticBean implements AuditListener { 045 046 /** The length of unicode placeholder. */ 047 private static final int UNICODE_LENGTH = 4; 048 049 /** Unicode escaping upper limit. */ 050 private static final int UNICODE_ESCAPE_UPPER_LIMIT = 0x1F; 051 052 /** Input stream buffer size. */ 053 private static final int BUFFER_SIZE = 1024; 054 055 /** The placeholder for message. */ 056 private static final String MESSAGE_PLACEHOLDER = "${message}"; 057 058 /** The placeholder for severity level. */ 059 private static final String SEVERITY_LEVEL_PLACEHOLDER = "${severityLevel}"; 060 061 /** The placeholder for uri. */ 062 private static final String URI_PLACEHOLDER = "${uri}"; 063 064 /** The placeholder for line. */ 065 private static final String LINE_PLACEHOLDER = "${line}"; 066 067 /** The placeholder for column. */ 068 private static final String COLUMN_PLACEHOLDER = "${column}"; 069 070 /** The placeholder for rule id. */ 071 private static final String RULE_ID_PLACEHOLDER = "${ruleId}"; 072 073 /** The placeholder for version. */ 074 private static final String VERSION_PLACEHOLDER = "${version}"; 075 076 /** The placeholder for results. */ 077 private static final String RESULTS_PLACEHOLDER = "${results}"; 078 079 /** Helper writer that allows easy encoding and printing. */ 080 private final PrintWriter writer; 081 082 /** Close output stream in auditFinished. */ 083 private final boolean closeStream; 084 085 /** The results. */ 086 private final List<String> results = new ArrayList<>(); 087 088 /** Content for the entire report. */ 089 private final String report; 090 091 /** Content for result representing an error with source line and column. */ 092 private final String resultLineColumn; 093 094 /** Content for result representing an error with source line only. */ 095 private final String resultLineOnly; 096 097 /** Content for result representing an error with filename only and without source location. */ 098 private final String resultFileOnly; 099 100 /** Content for result representing an error without filename or location. */ 101 private final String resultErrorOnly; 102 103 /** 104 * Creates a new {@code SarifLogger} instance. 105 * 106 * @param outputStream where to log audit events 107 * @param outputStreamOptions if {@code CLOSE} that should be closed in auditFinished() 108 * @throws IllegalArgumentException if outputStreamOptions is null 109 * @throws IOException if there is reading errors. 110 */ 111 public SarifLogger( 112 OutputStream outputStream, 113 OutputStreamOptions outputStreamOptions) throws IOException { 114 if (outputStreamOptions == null) { 115 throw new IllegalArgumentException("Parameter outputStreamOptions can not be null"); 116 } 117 writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)); 118 closeStream = outputStreamOptions == OutputStreamOptions.CLOSE; 119 report = readResource("/com/puppycrawl/tools/checkstyle/sarif/SarifReport.template"); 120 resultLineColumn = 121 readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultLineColumn.template"); 122 resultLineOnly = 123 readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultLineOnly.template"); 124 resultFileOnly = 125 readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultFileOnly.template"); 126 resultErrorOnly = 127 readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultErrorOnly.template"); 128 } 129 130 @Override 131 protected void finishLocalSetup() { 132 // No code by default 133 } 134 135 @Override 136 public void auditStarted(AuditEvent event) { 137 // No code by default 138 } 139 140 /** 141 * {@inheritDoc} 142 * Following idea suppressions are false positives 143 * 144 * @noinspection DynamicRegexReplaceableByCompiledPattern 145 */ 146 @Override 147 public void auditFinished(AuditEvent event) { 148 final String version = SarifLogger.class.getPackage().getImplementationVersion(); 149 final String rendered = report 150 .replace(VERSION_PLACEHOLDER, String.valueOf(version)) 151 .replace(RESULTS_PLACEHOLDER, String.join(",\n", results)); 152 writer.print(rendered); 153 if (closeStream) { 154 writer.close(); 155 } 156 else { 157 writer.flush(); 158 } 159 } 160 161 /** 162 * {@inheritDoc} 163 * Following idea suppressions are false positives 164 * 165 * @noinspection DynamicRegexReplaceableByCompiledPattern 166 */ 167 @Override 168 public void addError(AuditEvent event) { 169 if (event.getColumn() > 0) { 170 results.add(resultLineColumn 171 .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel())) 172 .replace(URI_PLACEHOLDER, event.getFileName()) 173 .replace(COLUMN_PLACEHOLDER, Integer.toString(event.getColumn())) 174 .replace(LINE_PLACEHOLDER, Integer.toString(event.getLine())) 175 .replace(MESSAGE_PLACEHOLDER, escape(event.getMessage())) 176 .replace(RULE_ID_PLACEHOLDER, event.getViolation().getKey()) 177 ); 178 } 179 else { 180 results.add(resultLineOnly 181 .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel())) 182 .replace(URI_PLACEHOLDER, event.getFileName()) 183 .replace(LINE_PLACEHOLDER, Integer.toString(event.getLine())) 184 .replace(MESSAGE_PLACEHOLDER, escape(event.getMessage())) 185 .replace(RULE_ID_PLACEHOLDER, event.getViolation().getKey()) 186 ); 187 } 188 } 189 190 /** 191 * {@inheritDoc} 192 * Following idea suppressions are false positives 193 * 194 * @noinspection DynamicRegexReplaceableByCompiledPattern 195 */ 196 @Override 197 public void addException(AuditEvent event, Throwable throwable) { 198 final StringWriter stringWriter = new StringWriter(); 199 final PrintWriter printer = new PrintWriter(stringWriter); 200 throwable.printStackTrace(printer); 201 if (event.getFileName() == null) { 202 results.add(resultErrorOnly 203 .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel())) 204 .replace(MESSAGE_PLACEHOLDER, escape(stringWriter.toString())) 205 ); 206 } 207 else { 208 results.add(resultFileOnly 209 .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel())) 210 .replace(URI_PLACEHOLDER, event.getFileName()) 211 .replace(MESSAGE_PLACEHOLDER, escape(stringWriter.toString())) 212 ); 213 } 214 } 215 216 @Override 217 public void fileStarted(AuditEvent event) { 218 // No need to implement this method in this class 219 } 220 221 @Override 222 public void fileFinished(AuditEvent event) { 223 // No need to implement this method in this class 224 } 225 226 /** 227 * Render the severity level into SARIF severity level. 228 * 229 * @param severityLevel the Severity level. 230 * @return the rendered severity level in string. 231 */ 232 private static String renderSeverityLevel(SeverityLevel severityLevel) { 233 final String renderedSeverityLevel; 234 switch (severityLevel) { 235 case IGNORE: 236 renderedSeverityLevel = "none"; 237 break; 238 case INFO: 239 renderedSeverityLevel = "note"; 240 break; 241 case WARNING: 242 renderedSeverityLevel = "warning"; 243 break; 244 case ERROR: 245 default: 246 renderedSeverityLevel = "error"; 247 break; 248 } 249 return renderedSeverityLevel; 250 } 251 252 /** 253 * Escape \b, \f, \n, \r, \t, \", \\ and U+0000 through U+001F. 254 * Reference: https://www.ietf.org/rfc/rfc4627.txt - 2.5. Strings 255 * 256 * @param value the value to escape. 257 * @return the escaped value if necessary. 258 */ 259 public static String escape(String value) { 260 final StringBuilder sb = new StringBuilder(value.length()); 261 for (int i = 0; i < value.length(); i++) { 262 final char chr = value.charAt(i); 263 switch (chr) { 264 case '"': 265 sb.append("\\\""); 266 break; 267 case '\\': 268 sb.append("\\\\"); 269 break; 270 case '\b': 271 sb.append("\\b"); 272 break; 273 case '\f': 274 sb.append("\\f"); 275 break; 276 case '\n': 277 sb.append("\\n"); 278 break; 279 case '\r': 280 sb.append("\\r"); 281 break; 282 case '\t': 283 sb.append("\\t"); 284 break; 285 case '/': 286 sb.append("\\/"); 287 break; 288 default: 289 if (chr <= UNICODE_ESCAPE_UPPER_LIMIT) { 290 sb.append(escapeUnicode1F(chr)); 291 } 292 else { 293 sb.append(chr); 294 } 295 break; 296 } 297 } 298 return sb.toString(); 299 } 300 301 /** 302 * Escape the character between 0x00 to 0x1F in JSON. 303 * 304 * @param chr the character to be escaped. 305 * @return the escaped string. 306 */ 307 private static String escapeUnicode1F(char chr) { 308 final StringBuilder stringBuilder = new StringBuilder(UNICODE_LENGTH + 1); 309 stringBuilder.append("\\u"); 310 final String hexString = Integer.toHexString(chr); 311 for (int i = 0; i < UNICODE_LENGTH - hexString.length(); i++) { 312 stringBuilder.append('0'); 313 } 314 stringBuilder.append(hexString.toUpperCase(Locale.US)); 315 return stringBuilder.toString(); 316 } 317 318 /** 319 * Read string from given resource. 320 * 321 * @param name name of the desired resource 322 * @return the string content from the give resource 323 * @throws IOException if there is reading errors 324 */ 325 public static String readResource(String name) throws IOException { 326 try (InputStream inputStream = SarifLogger.class.getResourceAsStream(name); 327 ByteArrayOutputStream result = new ByteArrayOutputStream()) { 328 if (inputStream == null) { 329 throw new IOException("Cannot find the resource " + name); 330 } 331 final byte[] buffer = new byte[BUFFER_SIZE]; 332 int length = inputStream.read(buffer); 333 while (length != -1) { 334 result.write(buffer, 0, length); 335 length = inputStream.read(buffer); 336 } 337 return result.toString(StandardCharsets.UTF_8.name()); 338 } 339 } 340}