internal/platform/sarif.go (394 lines of code) (raw):

/* * Copyright 2021-2024 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package platform import ( "encoding/json" "fmt" "os" "path/filepath" "sort" "strings" "time" "github.com/JetBrains/qodana-cli/internal/platform/msg" "github.com/JetBrains/qodana-cli/internal/platform/qdenv" "github.com/JetBrains/qodana-cli/internal/platform/thirdpartyscan" "github.com/JetBrains/qodana-cli/internal/sarif" "github.com/google/uuid" bbapi "github.com/reviewdog/go-bitbucket" log "github.com/sirupsen/logrus" ) // https://www.jetbrains.com/help/qodana/qodana-sarif-output.html const ( baselineStateEmpty = "" // baselineStateEmpty default baseline state (not set) baselineStateNew = "new" // baselineStateNew new baseline state baselineStateUnchanged = "unchanged" // baselineStateUnchanged unchanged baseline state extension = ".sarif.json" qodanaCritical = "Critical" qodanaHigh = "High" qodanaModerate = "Moderate" qodanaLow = "Low" qodanaInfo = "Info" sarifError = "error" sarifWarning = "warning" sarifNote = "note" ) func MergeSarifReports(c thirdpartyscan.Context, deviceId string) (int, error) { tmpResultsDir := GetTmpResultsDir(c.ResultsDir()) files, err := findSarifFiles(tmpResultsDir) sort.Strings(files) if err != nil { return 0, fmt.Errorf("error locating SARIF files: %s", err) } if len(files) == 0 { return 0, fmt.Errorf("no SARIF files (file names ending with .sarif.json) found in %s", tmpResultsDir) } ch := make(chan *sarif.Report) go collectReports(files, ch) finalReport, err := mergeReports(ch) if err != nil || finalReport == nil { return 0, fmt.Errorf("error merging SARIF files: %s", err) } for _, result := range finalReport.Runs[0].Results { // update locations[].physicalLocation.artifactLocation.uri by removing the projectDir prefix for _, location := range result.Locations { if (location.PhysicalLocation == nil) || (location.PhysicalLocation.ArtifactLocation == nil) { continue } toReplace := c.ProjectDir() if !strings.HasSuffix(toReplace, string(os.PathSeparator)) { toReplace += string(os.PathSeparator) } location.PhysicalLocation.ArtifactLocation.Uri = strings.TrimPrefix( location.PhysicalLocation.ArtifactLocation.Uri, toReplace, ) } } finalReport.Runs[0].Results = removeDuplicates(finalReport.Runs[0].Results) SetVersionControlParams(c, deviceId, finalReport) totalProblems := len(finalReport.Runs[0].Results) err = WriteReport(GetSarifPath(c.ResultsDir()), finalReport) if err != nil { return 0, err } return totalProblems, nil } func removeDuplicates(results []sarif.Result) []sarif.Result { if len(results) == 0 { return results } seen := make(map[string]struct{}, len(results)) writeIndex := 0 for _, result := range results { if result.PartialFingerprints != nil { fingerPrint := getFingerprint(&result) if fingerPrint != "" { if _, exists := seen[fingerPrint]; exists { continue } seen[fingerPrint] = struct{}{} } } results[writeIndex] = result writeIndex++ } if len(results) != writeIndex { log.Warnf("Removed duplicates: %d", len(results)-writeIndex) } return results[:writeIndex] } func WriteReport(path string, finalReport *sarif.Report) error { // serialize object skipping empty fields fatBytes, err := json.MarshalIndent(finalReport, "", " ") if err != nil { return fmt.Errorf("error marshalling report: %s", err) } f, err := os.Create(path) if err != nil { return fmt.Errorf("error creating resulting SARIF file: %s", err) } defer func(f *os.File) { err := f.Close() if err != nil { fmt.Printf("error closing resulting SARIF file: %s\n", err) } }(f) _, err = f.Write(fatBytes) if err != nil { return fmt.Errorf("error writing resulting SARIF file: %s", err) } return nil } func MakeShortSarif(sarifPath string, shortSarifPath string) error { report, err := ReadReport(sarifPath) if err != nil { return err } if len(report.Runs) == 0 { return fmt.Errorf("error reading SARIF %s: no runs found", sarifPath) } report.Runs[0].Tool.Extensions = []sarif.ToolComponent{} report.Runs[0].Tool.Driver.Taxa = []sarif.ReportingDescriptor{} report.Runs[0].Tool.Driver.Rules = []sarif.ReportingDescriptor{} report.Runs[0].Results = []sarif.Result{} report.Runs[0].Artifacts = []sarif.Artifact{} return WriteReport(shortSarifPath, report) } func SetVersionControlParams(c thirdpartyscan.Context, deviceId string, finalReport *sarif.Report) { linterInfo := c.LinterInfo() vcd, err := GetVersionDetails(c.ProjectDir()) if err != nil { log.Errorf("Error getting version control details: %s. Project is probably outside of the Git VCS.", err) } else { finalReport.Runs[0].VersionControlProvenance = make([]sarif.VersionControlDetails, 0) finalReport.Runs[0].VersionControlProvenance = append(finalReport.Runs[0].VersionControlProvenance, vcd) } if deviceId != "" { finalReport.Runs[0].Properties = &sarif.PropertyBag{} finalReport.Runs[0].Properties.AdditionalProperties = map[string]interface{}{ "deviceId": deviceId, } } if linterInfo.ProductCode != "" { finalReport.Runs[0].Tool.Driver.Name = linterInfo.ProductCode } if linterInfo.LinterPresentableName != "" { finalReport.Runs[0].Tool.Driver.FullName = linterInfo.LinterPresentableName } if linterInfo.LinterVersion != "" { finalReport.Runs[0].Tool.Driver.Version = linterInfo.LinterVersion } finalReport.Runs[0].AutomationDetails = &sarif.RunAutomationDetails{ Guid: RunGUID(), Id: ReportId(linterInfo.ProductCode), Properties: &sarif.PropertyBag{ AdditionalProperties: map[string]interface{}{ "jobUrl": JobUrl(), }, }, } } func findSarifFiles(root string) ([]string, error) { var files []string err := filepath.Walk( root, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if !info.IsDir() && strings.HasSuffix(strings.ToLower(info.Name()), extension) { files = append(files, path) } return nil }, ) if err != nil { return nil, err } return files, nil } func collectReports(files []string, ch chan<- *sarif.Report) { for _, file := range files { r, err := ReadReport(file) if err != nil { fmt.Printf("Error reading SARIF %s: %s\n", file, err) continue } ch <- r } close(ch) } func ReadReport(file string) (*sarif.Report, error) { f, err := os.Open(file) if err != nil { return nil, err } defer func(f *os.File) { err := f.Close() if err != nil { fmt.Printf("Error closing SARIF file %s: %s\n", file, err) } }(f) dec := json.NewDecoder(f) var r sarif.Report if err := dec.Decode(&r); err != nil { return nil, err } return &r, nil } func ReadReportFromString(sarifStr string) (*sarif.Report, error) { var r sarif.Report if err := json.Unmarshal([]byte(sarifStr), &r); err != nil { return nil, err } return &r, nil } func mergeReports(ch <-chan *sarif.Report) (*sarif.Report, error) { var finalReport *sarif.Report for r := range ch { if finalReport == nil { // For the first file, keep the toolDesc configuration and initialize the 'Runs' slice finalReport = &sarif.Report{ Schema: r.Schema, Version: r.Version, Runs: make([]sarif.Run, 0, len(r.Runs)), } finalReport.Runs = append(finalReport.Runs, r.Runs[0]) finalReport.Runs[0].Results = r.Runs[0].Results finalReport.Runs[0].Tool = r.Runs[0].Tool continue } // Append results from each report into the 'Results' slice of the first run of the final report for _, run := range r.Runs { finalReport.Runs[0].Results = append(finalReport.Runs[0].Results, run.Results...) finalReport.Runs[0].Artifacts = append(finalReport.Runs[0].Artifacts, run.Artifacts...) } } return finalReport, nil } func RunGUID() string { runGUID := os.Getenv("QODANA_AUTOMATION_GUID") if runGUID == "" { runGUID = uuid.New().String() } return runGUID } func ReportId(projectName string) string { reportId := os.Getenv("QODANA_REPORT_ID") if reportId != "" { return reportId } projectId := os.Getenv("QODANA_PROJECT_ID") if projectId == "" { projectId = projectName } date := time.Now().Format("2006-01-02") tool := "qodana" return projectId + "/" + tool + "/" + date } func JobUrl() string { return os.Getenv("QODANA_JOB_URL") } func getRuleDescription(report *sarif.Report, ruleId string) string { for _, run := range report.Runs { for _, extension := range run.Tool.Extensions { for _, rule := range extension.Rules { if rule.Id == ruleId { return rule.ShortDescription.Text } } } } return "" } // ProcessSarif concludes the result of analysis based on provided SARIF file // - can print problems to the output // - can create GitLab CodeQuality issues report // - can submit problems to BitBucket Code Insights func ProcessSarif(sarifPath, analysisId, reportUrl string, printProblems, codeClimate, codeInsights bool) { newProblems := 0 s, err := ReadReport(sarifPath) if err != nil { log.Fatal(err) } var codeClimateIssues = make([]CCIssue, 0) var codeInsightIssues = make([]bbapi.ReportAnnotation, 0) rulesDescriptions := make(map[string]string) if printProblems { msg.EmptyMessage() } for _, run := range s.Runs { for _, r := range run.Results { ruleId := r.RuleId message := r.Message.Text baselineState := baselineStateEmpty if r.BaselineState != nil { baselineState = r.BaselineState.(string) } if baselineState == baselineStateNew || baselineState == baselineStateEmpty { newProblems++ } if len(r.Locations) > 0 && baselineState != baselineStateUnchanged { if codeClimate { codeClimateIssues = append(codeClimateIssues, sarifResultToCodeClimate(&r)) } if codeInsights { ruleDescription, ok := rulesDescriptions[ruleId] if !ok { ruleDescription = getRuleDescription(s, ruleId) rulesDescriptions[ruleId] = ruleDescription } codeInsightIssues = append(codeInsightIssues, buildAnnotation(&r, ruleDescription, reportUrl)) } if printProblems { err = printSarifProblem(&r, ruleId, message) if err != nil { log.Debugf("Failed to print result: %s", err) } } } } } if codeClimate { err = writeGlCodeQualityReport(codeClimateIssues, sarifPath) if err != nil { log.Warnf("Problems writing GitLab CodeQuality report: %v", err) } } if codeInsights { err = sendBitBucketReport(codeInsightIssues, s.Runs[0].Tool.Driver.FullName, reportUrl, "qodana-"+analysisId) if err != nil { log.Warnf("Problems sending BitBucket Code Insights report: %v", err) } } if !qdenv.IsContainer() { if newProblems == 0 { msg.SuccessMessage(msg.GetProblemsFoundMessage(0)) } else { msg.ErrorMessage(msg.GetProblemsFoundMessage(newProblems)) } } } func printSarifProblem(r *sarif.Result, ruleId, message string) error { if r == nil { return fmt.Errorf("r must not be nil") } path := "" line := 0 column := 0 contextLine := 0 contextText := "" if len(r.Locations) > 0 && r.Locations[0].PhysicalLocation != nil { location := r.Locations[0].PhysicalLocation if location.ArtifactLocation != nil { path = location.ArtifactLocation.Uri } if location.Region != nil { line = int(location.Region.StartLine) column = int(location.Region.StartColumn) } if location.ContextRegion != nil { contextLine = int(location.ContextRegion.StartLine) if location.ContextRegion.Snippet != nil { contextText = location.ContextRegion.Snippet.Text } } } msg.PrintProblem(ruleId, getSeverity(r), message, path, line, column, contextLine, contextText) return nil } // getFingerprint returns the fingerprint of the Qodana (or not) SARIF result. func getFingerprint(r *sarif.Result) string { if r != nil && r.PartialFingerprints != nil { fingerprint, ok := r.PartialFingerprints["equalIndicator/v2"] if ok { return fingerprint } fingerprint, ok = r.PartialFingerprints["equalIndicator/v1"] if ok { return fingerprint } } log.Fatalf("failed to get fingerprint from result: %v", r) return "" } // getSeverity returns the severity of the Qodana (or not) SARIF result. func getSeverity(r *sarif.Result) string { if r.Properties != nil && r.Properties.AdditionalProperties != nil { severity, ok := r.Properties.AdditionalProperties["qodanaSeverity"].(string) if ok { return severity } } if r.Level != nil { log.Debug("failed to get severity from properties, using sarif level") return r.Level.(string) } return sarifNote }