src/Analyzer.Reports/SarifReportWriter.cs (193 lines of code) (raw):

// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.IO.Abstractions; using System.Linq; using Microsoft.Azure.Templates.Analyzer.Types; using Microsoft.CodeAnalysis.Sarif; using Microsoft.CodeAnalysis.Sarif.Writers; using SarifResult = Microsoft.CodeAnalysis.Sarif.Result; namespace Microsoft.Azure.Templates.Analyzer.Reports { /// <summary> /// Class to export analysis result to SARIF report /// </summary> public class SarifReportWriter : IReportWriter { internal const string UriBaseIdString = "ROOTPATH"; internal const string PeriodString = "."; private readonly List<string> filesAlreadyOutput = new List<string>(); private readonly IFileInfo reportFile; private readonly Stream reportFileStream; private readonly StreamWriter outputTextWriter; private Run sarifRun; private readonly IDictionary<string, ReportingDescriptor> rulesDictionary; private string rootPath; private int totalResults = 0; /// <summary> /// Logger used to output information to the SARIF file /// </summary> public SarifLogger SarifLogger { get; set; } /// <summary> /// Constructor of the SarifReportWriter class /// </summary> /// <param name="reportFile">File where the report will be written</param> /// <param name="targetPath">The directory that will be analyzed</param> public SarifReportWriter(IFileInfo reportFile, string targetPath = null) { this.reportFile = reportFile ?? throw new ArgumentException(nameof(reportFile)); this.reportFileStream = this.reportFile.Create(); this.outputTextWriter = new StreamWriter(this.reportFileStream); this.rulesDictionary = new ConcurrentDictionary<string, ReportingDescriptor>(); this.InitRun(); this.RootPath = targetPath; this.SarifLogger = new SarifLogger( textWriter: this.outputTextWriter, logFilePersistenceOptions: LogFilePersistenceOptions.PrettyPrint | LogFilePersistenceOptions.OverwriteExistingOutputFile, tool: this.sarifRun.Tool, run: this.sarifRun, levels: new List<FailureLevel> { FailureLevel.Warning, FailureLevel.Error, FailureLevel.Note }, kinds: new List<ResultKind> { ResultKind.Fail }); } /// <inheritdoc/> public void WriteResults(IEnumerable<IEvaluation> evaluations, IFileInfo templateFile, IFileInfo parameterFile = null) { this.RootPath ??= templateFile.DirectoryName; var resultsByFile = ReportsHelper.GetResultsByFile(evaluations, filesAlreadyOutput); // output files in sorted order, but always output root first var filesWithResults = resultsByFile.Keys.ToList(); filesWithResults.Sort(); int rootIndex = filesWithResults.IndexOf(templateFile.FullName); if (rootIndex != -1) { filesWithResults.RemoveAt(rootIndex); filesWithResults.Insert(0, templateFile.FullName); } foreach (var fileWithResults in filesWithResults) { // add analysis target if result not in root template ArtifactLocation analysisTarget = null; if (fileWithResults != templateFile.FullName) { (var pathBelongsToRoot, var filePath) = GetFilePathInfo(templateFile.FullName); analysisTarget = new ArtifactLocation { Uri = new Uri( UriHelper.MakeValidUri(filePath), UriKind.RelativeOrAbsolute), UriBaseId = pathBelongsToRoot ? UriBaseIdString : null, }; } foreach ((var evaluation, var failedResults) in resultsByFile[fileWithResults]) { // get rule definition from first level evaluation this.ExtractRule(evaluation); // create location for each individual result (var pathBelongsToRoot, var filePath) = GetFilePathInfo(fileWithResults); var locations = failedResults.Select(result => new Location { PhysicalLocation = new PhysicalLocation { ArtifactLocation = new ArtifactLocation { Uri = new Uri( UriHelper.MakeValidUri(filePath), UriKind.RelativeOrAbsolute), UriBaseId = pathBelongsToRoot ? UriBaseIdString : null, }, Region = new Region { StartLine = result.SourceLocation.LineNumber }, }, }).ToList(); // Log result SarifLogger.Log(this.rulesDictionary[evaluation.RuleId], new SarifResult { RuleId = evaluation.RuleId, Kind = evaluation.Passed ? ResultKind.Pass : ResultKind.Fail, // Check Passed property in case we support outputting passed results in future Level = evaluation.Passed ? FailureLevel.None : GetLevelFromSeverity(evaluation.Severity), Message = new Message { Id = "default" }, // should be customized message for each result Locations = locations, AnalysisTarget = analysisTarget, }); totalResults++; } } filesAlreadyOutput.AddRange(filesWithResults); } internal string RootPath { get => this.rootPath; set { if (string.IsNullOrWhiteSpace(rootPath) && !string.IsNullOrWhiteSpace(value)) { this.rootPath = value; if (!this.sarifRun.OriginalUriBaseIds.ContainsKey(UriBaseIdString)) { this.sarifRun.OriginalUriBaseIds.Add( UriBaseIdString, new ArtifactLocation { Uri = new Uri(UriHelper.MakeValidUri(rootPath), UriKind.RelativeOrAbsolute) }); } } } } internal Run SarifRun => this.sarifRun; private void InitRun() { this.sarifRun = new Run { Tool = new Tool { Driver = new ToolComponent { Name = Constants.ToolName, FullName = Constants.ToolFullName, Version = Constants.ToolVersion, InformationUri = new Uri(Constants.InformationUri), Organization = Constants.Organization, } }, OriginalUriBaseIds = new Dictionary<string, ArtifactLocation>(), }; } private void ExtractRule(IEvaluation evaluation) { if (rulesDictionary.ContainsKey(evaluation.RuleId)) return; var hasUri = Uri.TryCreate(evaluation.HelpUri, UriKind.RelativeOrAbsolute, out Uri uri); rulesDictionary.Add(evaluation.RuleId, new ReportingDescriptor { Id = evaluation.RuleId, Name = evaluation.RuleName, ShortDescription = new MultiformatMessageString { Text = AppendPeriod(evaluation.RuleShortDescription) }, FullDescription = new MultiformatMessageString { Text = AppendPeriod(evaluation.RuleFullDescription) }, Help = new MultiformatMessageString { Text = AppendPeriod(evaluation.Recommendation) }, HelpUri = hasUri ? uri : null, MessageStrings = new Dictionary<string, MultiformatMessageString>() { { "default", new MultiformatMessageString { Text = AppendPeriod(evaluation.RuleFullDescription) } } }, DefaultConfiguration = new ReportingConfiguration { Level = GetLevelFromSeverity(evaluation.Severity) } }); } private (bool, string) GetFilePathInfo(string fileFullName) { bool isFileInRootPath = IsSubPath(this.RootPath, fileFullName); string filePath = isFileInRootPath ? Path.GetRelativePath(this.RootPath, fileFullName) : fileFullName; return (isFileInRootPath, filePath); } private static FailureLevel GetLevelFromSeverity(Severity severity) => severity switch { Severity.High => FailureLevel.Error, Severity.Medium => FailureLevel.Warning, _ => FailureLevel.Note, }; internal static bool IsSubPath(string rootPath, string childFilePath) { var relativePath = Path.GetRelativePath(rootPath, childFilePath); return !relativePath.StartsWith('.') && !Path.IsPathRooted(relativePath); } internal static string AppendPeriod(string text) => text == null ? string.Empty : text.EndsWith(PeriodString, StringComparison.OrdinalIgnoreCase) ? text : text + PeriodString; /// <inheritdoc/> public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } /// <summary> /// Disposes resources owned by this instance. /// </summary> /// <param name="disposing"></param> public void Dispose(bool disposing) { this.SarifLogger?.Dispose(); this.outputTextWriter?.Dispose(); this.reportFileStream?.Dispose(); Console.WriteLine($"{Environment.NewLine}Wrote {totalResults} {(totalResults == 1 ? "result" : "results")} to {reportFile.FullName}"); } } }