1   
2   
3   
4   
5   
6   
7   
8   
9   
10  
11  
12  
13  
14  
15  
16  
17  
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.HashMap;
32  import java.util.LinkedHashMap;
33  import java.util.List;
34  import java.util.Locale;
35  import java.util.Map;
36  import java.util.MissingResourceException;
37  import java.util.ResourceBundle;
38  import java.util.regex.Pattern;
39  
40  import com.puppycrawl.tools.checkstyle.api.AuditEvent;
41  import com.puppycrawl.tools.checkstyle.api.AuditListener;
42  import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
43  import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
44  import com.puppycrawl.tools.checkstyle.meta.ModuleDetails;
45  import com.puppycrawl.tools.checkstyle.meta.XmlMetaReader;
46  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
47  
48  
49  
50  
51  
52  
53  public class SarifLogger extends AbstractAutomaticBean implements AuditListener {
54  
55      
56      private static final int UNICODE_LENGTH = 4;
57  
58      
59      private static final int UNICODE_ESCAPE_UPPER_LIMIT = 0x1F;
60  
61      
62      private static final int BUFFER_SIZE = 1024;
63  
64      
65      private static final String MESSAGE_PLACEHOLDER = "${message}";
66  
67      
68      private static final String MESSAGE_TEXT_PLACEHOLDER = "${messageText}";
69  
70      
71      private static final String MESSAGE_ID_PLACEHOLDER = "${messageId}";
72  
73      
74      private static final String SEVERITY_LEVEL_PLACEHOLDER = "${severityLevel}";
75  
76      
77      private static final String URI_PLACEHOLDER = "${uri}";
78  
79      
80      private static final String LINE_PLACEHOLDER = "${line}";
81  
82      
83      private static final String COLUMN_PLACEHOLDER = "${column}";
84  
85      
86      private static final String RULE_ID_PLACEHOLDER = "${ruleId}";
87  
88      
89      private static final String VERSION_PLACEHOLDER = "${version}";
90  
91      
92      private static final String RESULTS_PLACEHOLDER = "${results}";
93  
94      
95      private static final String RULES_PLACEHOLDER = "${rules}";
96  
97      
98      private static final String TWO_BACKSLASHES = "\\\\";
99  
100     
101     private static final Pattern A_SPACE_PATTERN = Pattern.compile(" ");
102 
103     
104     private static final Pattern TWO_BACKSLASHES_PATTERN = Pattern.compile(TWO_BACKSLASHES);
105 
106     
107     private static final Pattern WINDOWS_DRIVE_LETTER_PATTERN =
108             Pattern.compile("\\A[A-Z]:", Pattern.CASE_INSENSITIVE);
109 
110     
111     private static final String COMMA_LINE_SEPARATOR = ",\n";
112 
113     
114     private final PrintWriter writer;
115 
116     
117     private final boolean closeStream;
118 
119     
120     private final List<String> results = new ArrayList<>();
121 
122     
123     private final Map<String, ModuleDetails> allModuleMetadata = new HashMap<>();
124 
125     
126     private final Map<RuleKey, ModuleDetails> ruleMetadata = new LinkedHashMap<>();
127 
128     
129     private final String report;
130 
131     
132     private final String resultLineColumn;
133 
134     
135     private final String resultLineOnly;
136 
137     
138     private final String resultFileOnly;
139 
140     
141     private final String resultErrorOnly;
142 
143     
144     private final String rule;
145 
146     
147     private final String messageStrings;
148 
149     
150     private final String messageTextOnly;
151 
152     
153     private final String messageWithId;
154 
155     
156 
157 
158 
159 
160 
161 
162 
163 
164 
165 
166     public SarifLogger(
167         OutputStream outputStream,
168         AutomaticBean.OutputStreamOptions outputStreamOptions) throws IOException {
169         this(outputStream, OutputStreamOptions.valueOf(outputStreamOptions.name()));
170     }
171 
172     
173 
174 
175 
176 
177 
178 
179 
180     public SarifLogger(
181         OutputStream outputStream,
182         OutputStreamOptions outputStreamOptions) throws IOException {
183         if (outputStreamOptions == null) {
184             throw new IllegalArgumentException("Parameter outputStreamOptions can not be null");
185         }
186         writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
187         closeStream = outputStreamOptions == OutputStreamOptions.CLOSE;
188         loadModuleMetadata();
189         report = readResource("/com/puppycrawl/tools/checkstyle/sarif/SarifReport.template");
190         resultLineColumn =
191             readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultLineColumn.template");
192         resultLineOnly =
193             readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultLineOnly.template");
194         resultFileOnly =
195             readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultFileOnly.template");
196         resultErrorOnly =
197             readResource("/com/puppycrawl/tools/checkstyle/sarif/ResultErrorOnly.template");
198         rule = readResource("/com/puppycrawl/tools/checkstyle/sarif/Rule.template");
199         messageStrings =
200             readResource("/com/puppycrawl/tools/checkstyle/sarif/MessageStrings.template");
201         messageTextOnly =
202             readResource("/com/puppycrawl/tools/checkstyle/sarif/MessageTextOnly.template");
203         messageWithId =
204             readResource("/com/puppycrawl/tools/checkstyle/sarif/MessageWithId.template");
205     }
206 
207     
208 
209 
210     private void loadModuleMetadata() {
211         final List<ModuleDetails> allModules =
212                 XmlMetaReader.readAllModulesIncludingThirdPartyIfAny();
213         for (ModuleDetails module : allModules) {
214             allModuleMetadata.put(module.getFullQualifiedName(), module);
215         }
216     }
217 
218     @Override
219     protected void finishLocalSetup() {
220         
221     }
222 
223     @Override
224     public void auditStarted(AuditEvent event) {
225         
226     }
227 
228     @Override
229     public void auditFinished(AuditEvent event) {
230         String rendered = replaceVersionString(report);
231         rendered = rendered
232                 .replace(RESULTS_PLACEHOLDER, String.join(COMMA_LINE_SEPARATOR, results))
233                 .replace(RULES_PLACEHOLDER, String.join(COMMA_LINE_SEPARATOR, generateRules()));
234         writer.print(rendered);
235         if (closeStream) {
236             writer.close();
237         }
238         else {
239             writer.flush();
240         }
241     }
242 
243     
244 
245 
246 
247 
248     private List<String> generateRules() {
249         final List<String> result = new ArrayList<>();
250         for (Map.Entry<RuleKey, ModuleDetails> entry : ruleMetadata.entrySet()) {
251             final RuleKey ruleKey = entry.getKey();
252             final ModuleDetails module = entry.getValue();
253             final String shortDescription;
254             final String fullDescription;
255             final String messageStringsFragment;
256             if (module == null) {
257                 shortDescription = CommonUtil.baseClassName(ruleKey.sourceName());
258                 fullDescription = "No description available";
259                 messageStringsFragment = "";
260             }
261             else {
262                 shortDescription = module.getName();
263                 fullDescription = module.getDescription();
264                 messageStringsFragment = String.join(COMMA_LINE_SEPARATOR,
265                         generateMessageStrings(module));
266             }
267             result.add(rule
268                     .replace(RULE_ID_PLACEHOLDER, ruleKey.toRuleId())
269                     .replace("${shortDescription}", shortDescription)
270                     .replace("${fullDescription}", escape(fullDescription))
271                     .replace("${messageStrings}", messageStringsFragment));
272         }
273         return result;
274     }
275 
276     
277 
278 
279 
280 
281 
282     private List<String> generateMessageStrings(ModuleDetails module) {
283         final Map<String, String> messages = getMessages(module);
284         return module.getViolationMessageKeys().stream()
285                 .filter(messages::containsKey).map(key -> {
286                     final String message = messages.get(key);
287                     return messageStrings
288                             .replace("${key}", key)
289                             .replace("${text}", escape(message));
290                 }).toList();
291     }
292 
293     
294 
295 
296 
297 
298 
299     private static Map<String, String> getMessages(ModuleDetails moduleDetails) {
300         final String fullQualifiedName = moduleDetails.getFullQualifiedName();
301         final Map<String, String> result = new LinkedHashMap<>();
302         try {
303             final int lastDot = fullQualifiedName.lastIndexOf('.');
304             final String packageName = fullQualifiedName.substring(0, lastDot);
305             final String bundleName = packageName + ".messages";
306             final Class<?> moduleClass = Class.forName(fullQualifiedName);
307             final ResourceBundle bundle = ResourceBundle.getBundle(
308                     bundleName,
309                     Locale.ROOT,
310                     moduleClass.getClassLoader(),
311                     new LocalizedMessage.Utf8Control()
312             );
313             for (String key : moduleDetails.getViolationMessageKeys()) {
314                 result.put(key, bundle.getString(key));
315             }
316         }
317         catch (ClassNotFoundException | MissingResourceException ignored) {
318             
319             
320         }
321         return result;
322     }
323 
324     
325 
326 
327 
328 
329 
330     private static String replaceVersionString(String report) {
331         final String version = SarifLogger.class.getPackage().getImplementationVersion();
332         return report.replace(VERSION_PLACEHOLDER, String.valueOf(version));
333     }
334 
335     @Override
336     public void addError(AuditEvent event) {
337         final RuleKey ruleKey = cacheRuleMetadata(event);
338         final String message = generateMessage(ruleKey, event);
339         if (event.getColumn() > 0) {
340             results.add(resultLineColumn
341                 .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel()))
342                 .replace(URI_PLACEHOLDER, renderFileNameUri(event.getFileName()))
343                 .replace(COLUMN_PLACEHOLDER, Integer.toString(event.getColumn()))
344                 .replace(LINE_PLACEHOLDER, Integer.toString(event.getLine()))
345                 .replace(MESSAGE_PLACEHOLDER, message)
346                 .replace(RULE_ID_PLACEHOLDER, ruleKey.toRuleId())
347             );
348         }
349         else {
350             results.add(resultLineOnly
351                 .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel()))
352                 .replace(URI_PLACEHOLDER, renderFileNameUri(event.getFileName()))
353                 .replace(LINE_PLACEHOLDER, Integer.toString(event.getLine()))
354                 .replace(MESSAGE_PLACEHOLDER, message)
355                 .replace(RULE_ID_PLACEHOLDER, ruleKey.toRuleId())
356             );
357         }
358     }
359 
360     
361 
362 
363 
364 
365 
366     private RuleKey cacheRuleMetadata(AuditEvent event) {
367         final String sourceName = event.getSourceName();
368         final RuleKey key = new RuleKey(sourceName, event.getModuleId());
369         final ModuleDetails module = allModuleMetadata.get(sourceName);
370         ruleMetadata.putIfAbsent(key, module);
371         return key;
372     }
373 
374     
375 
376 
377 
378 
379 
380 
381     private String generateMessage(RuleKey ruleKey, AuditEvent event) {
382         final String violationKey = event.getViolation().getKey();
383         final ModuleDetails module = ruleMetadata.get(ruleKey);
384         final String result;
385         if (module != null && module.getViolationMessageKeys().contains(violationKey)) {
386             result = messageWithId
387                     .replace(MESSAGE_ID_PLACEHOLDER, violationKey)
388                     .replace(MESSAGE_TEXT_PLACEHOLDER, escape(event.getMessage()));
389         }
390         else {
391             result = messageTextOnly
392                     .replace(MESSAGE_TEXT_PLACEHOLDER, escape(event.getMessage()));
393         }
394         return result;
395     }
396 
397     @Override
398     public void addException(AuditEvent event, Throwable throwable) {
399         final StringWriter stringWriter = new StringWriter();
400         final PrintWriter printer = new PrintWriter(stringWriter);
401         throwable.printStackTrace(printer);
402         final String message = messageTextOnly
403                 .replace(MESSAGE_TEXT_PLACEHOLDER, escape(stringWriter.toString()));
404         if (event.getFileName() == null) {
405             results.add(resultErrorOnly
406                 .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel()))
407                 .replace(MESSAGE_PLACEHOLDER, message)
408             );
409         }
410         else {
411             results.add(resultFileOnly
412                 .replace(SEVERITY_LEVEL_PLACEHOLDER, renderSeverityLevel(event.getSeverityLevel()))
413                 .replace(URI_PLACEHOLDER, renderFileNameUri(event.getFileName()))
414                 .replace(MESSAGE_PLACEHOLDER, message)
415             );
416         }
417     }
418 
419     @Override
420     public void fileStarted(AuditEvent event) {
421         
422     }
423 
424     @Override
425     public void fileFinished(AuditEvent event) {
426         
427     }
428 
429     
430 
431 
432 
433 
434 
435     private static String renderFileNameUri(final String fileName) {
436         String normalized =
437                 A_SPACE_PATTERN
438                         .matcher(TWO_BACKSLASHES_PATTERN.matcher(fileName).replaceAll("/"))
439                         .replaceAll("%20");
440         if (WINDOWS_DRIVE_LETTER_PATTERN.matcher(normalized).find()) {
441             normalized = '/' + normalized;
442         }
443         return "file:" + normalized;
444     }
445 
446     
447 
448 
449 
450 
451 
452     private static String renderSeverityLevel(SeverityLevel severityLevel) {
453         return switch (severityLevel) {
454             case IGNORE -> "none";
455             case INFO -> "note";
456             case WARNING -> "warning";
457             case ERROR -> "error";
458         };
459     }
460 
461     
462 
463 
464 
465 
466 
467 
468     public static String escape(String value) {
469         final int length = value.length();
470         final StringBuilder sb = new StringBuilder(length);
471         for (int i = 0; i < length; i++) {
472             final char chr = value.charAt(i);
473             final String replacement = switch (chr) {
474                 case '"' -> "\\\"";
475                 case '\\' -> TWO_BACKSLASHES;
476                 case '\b' -> "\\b";
477                 case '\f' -> "\\f";
478                 case '\n' -> "\\n";
479                 case '\r' -> "\\r";
480                 case '\t' -> "\\t";
481                 case '/' -> "\\/";
482                 default -> {
483                     if (chr <= UNICODE_ESCAPE_UPPER_LIMIT) {
484                         yield escapeUnicode1F(chr);
485                     }
486                     yield Character.toString(chr);
487                 }
488             };
489             sb.append(replacement);
490         }
491 
492         return sb.toString();
493     }
494 
495     
496 
497 
498 
499 
500 
501     private static String escapeUnicode1F(char chr) {
502         final String hexString = Integer.toHexString(chr);
503         return "\\u"
504                 + "0".repeat(UNICODE_LENGTH - hexString.length())
505                 + hexString.toUpperCase(Locale.US);
506     }
507 
508     
509 
510 
511 
512 
513 
514 
515     public static String readResource(String name) throws IOException {
516         try (InputStream inputStream = SarifLogger.class.getResourceAsStream(name);
517              ByteArrayOutputStream result = new ByteArrayOutputStream()) {
518             if (inputStream == null) {
519                 throw new IOException("Cannot find the resource " + name);
520             }
521             final byte[] buffer = new byte[BUFFER_SIZE];
522             int length = 0;
523             while (length != -1) {
524                 result.write(buffer, 0, length);
525                 length = inputStream.read(buffer);
526             }
527             return result.toString(StandardCharsets.UTF_8);
528         }
529     }
530 
531     
532 
533 
534 
535 
536 
537     private record RuleKey(String sourceName, String moduleId) {
538         
539 
540 
541 
542 
543         private String toRuleId() {
544             final String result;
545             if (moduleId == null) {
546                 result = sourceName;
547             }
548             else {
549                 result = sourceName + '#' + moduleId;
550             }
551             return result;
552         }
553     }
554 }