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 @Override 141 public void auditFinished(AuditEvent event) { 142 final String version = SarifLogger.class.getPackage().getImplementationVersion(); 143 final String rendered = report 144 .replace(VERSION_PLACEHOLDER, String.valueOf(version)) 145 .replace(RESULTS_PLACEHOLDER, String.join(",\n", results)); 146 writer.print(rendered); 147 if (closeStream) { 148 writer.close(); 149 } 150 else { 151 writer.flush(); 152 } 153 } 154 155 @Override 156 public void addError(AuditEvent event) { 157 if (event.getColumn() > 0) { 158 results.add(resultLineColumn 159 .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel())) 160 .replace(URI_PLACEHOLDER, event.getFileName()) 161 .replace(COLUMN_PLACEHOLDER, Integer.toString(event.getColumn())) 162 .replace(LINE_PLACEHOLDER, Integer.toString(event.getLine())) 163 .replace(MESSAGE_PLACEHOLDER, escape(event.getMessage())) 164 .replace(RULE_ID_PLACEHOLDER, event.getViolation().getKey()) 165 ); 166 } 167 else { 168 results.add(resultLineOnly 169 .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel())) 170 .replace(URI_PLACEHOLDER, event.getFileName()) 171 .replace(LINE_PLACEHOLDER, Integer.toString(event.getLine())) 172 .replace(MESSAGE_PLACEHOLDER, escape(event.getMessage())) 173 .replace(RULE_ID_PLACEHOLDER, event.getViolation().getKey()) 174 ); 175 } 176 } 177 178 @Override 179 public void addException(AuditEvent event, Throwable throwable) { 180 final StringWriter stringWriter = new StringWriter(); 181 final PrintWriter printer = new PrintWriter(stringWriter); 182 throwable.printStackTrace(printer); 183 if (event.getFileName() == null) { 184 results.add(resultErrorOnly 185 .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel())) 186 .replace(MESSAGE_PLACEHOLDER, escape(stringWriter.toString())) 187 ); 188 } 189 else { 190 results.add(resultFileOnly 191 .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel())) 192 .replace(URI_PLACEHOLDER, event.getFileName()) 193 .replace(MESSAGE_PLACEHOLDER, escape(stringWriter.toString())) 194 ); 195 } 196 } 197 198 @Override 199 public void fileStarted(AuditEvent event) { 200 // No need to implement this method in this class 201 } 202 203 @Override 204 public void fileFinished(AuditEvent event) { 205 // No need to implement this method in this class 206 } 207 208 /** 209 * Render the severity level into SARIF severity level. 210 * 211 * @param severityLevel the Severity level. 212 * @return the rendered severity level in string. 213 */ 214 private static String renderSeverityLevel(SeverityLevel severityLevel) { 215 final String renderedSeverityLevel; 216 switch (severityLevel) { 217 case IGNORE: 218 renderedSeverityLevel = "none"; 219 break; 220 case INFO: 221 renderedSeverityLevel = "note"; 222 break; 223 case WARNING: 224 renderedSeverityLevel = "warning"; 225 break; 226 case ERROR: 227 default: 228 renderedSeverityLevel = "error"; 229 break; 230 } 231 return renderedSeverityLevel; 232 } 233 234 /** 235 * Escape \b, \f, \n, \r, \t, \", \\ and U+0000 through U+001F. 236 * Reference: https://www.ietf.org/rfc/rfc4627.txt - 2.5. Strings 237 * 238 * @param value the value to escape. 239 * @return the escaped value if necessary. 240 */ 241 public static String escape(String value) { 242 final StringBuilder sb = new StringBuilder(value.length()); 243 for (int i = 0; i < value.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}