Repository: reviewdog/reviewdog
Pull request #1787 (2024-06-16 13:10:35)

Support Sarif related locations
- Use github.com/haya14busa/go-sarif for sarif struct
- Support related locations with the SARIF parser

- [x] Updated Unreleased section in [CHANGELOG](https://github.com/reviewdog/reviewdog/blob/master/CHANGELOG.md) or it's not notable changes.




Patch:
diff --git a/README.md b/README.md
index c98cc6d4..e62df0fc 100644
--- a/README.md
+++ b/README.md
@@ -711,9 +711,7 @@ You can use public GitHub Actions to start using reviewdog with ease! :tada: :ar
   - [reviewdog/action-actionlint](https://github.com/reviewdog/action-actionlint) - Run [actionlint](https://github.com/rhysd/actionlint)
 - Protocol Buffers
   - [yoheimuta/action-protolint](https://github.com/yoheimuta/action-protolint) - Run [protolint](https://github.com/yoheimuta/protolint)
-- Rego
-  - [reviewdog/action-regal](https://github.com/reviewdog/action-regal) - Run [Regal](https://github.com/StyraInc/regal)
-
+  
 ... and more on [GitHub Marketplace](https://github.com/marketplace?utf8=✓&type=actions&query=reviewdog).
 
 Missing actions? Check out [reviewdog/action-template](https://github.com/reviewdog/action-template) and create a new reviewdog action!
diff --git a/go.mod b/go.mod
index b7d48660..cdc6fac9 100644
--- a/go.mod
+++ b/go.mod
@@ -55,6 +55,7 @@ require (
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
 	github.com/hashicorp/go-retryablehttp v0.7.2 // indirect
 	github.com/hashicorp/go-version v1.6.0 // indirect
+	github.com/haya14busa/go-sarif v0.0.0-20210102043135-e2c5fed2fa3d // indirect
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/prometheus/prometheus v2.5.0+incompatible // indirect
diff --git a/go.sum b/go.sum
index 831cc91a..b772f90f 100644
--- a/go.sum
+++ b/go.sum
@@ -215,6 +215,7 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
 github.com/haya14busa/go-actions-toolkit v0.0.0-20200105081403-ca0307860f01 h1:HiJF8Mek+I7PY0Bm+SuhkwaAZSZP83sw6rrTMrgZ0io=
 github.com/haya14busa/go-actions-toolkit v0.0.0-20200105081403-ca0307860f01/go.mod h1:1DWDZmeYf0LX30zscWb7K9rUMeirNeBMd5Dum+seUhc=
 github.com/haya14busa/go-checkstyle v0.0.0-20170303121022-5e9d09f51fa1/go.mod h1:RsN5RGgVYeXpcXNtWyztD5VIe7VNSEqpJvF2iEH7QvI=
+github.com/haya14busa/go-sarif v0.0.0-20210102043135-e2c5fed2fa3d h1:2uCbyMhWNFYI+HkRqXfIeTq5pTPxQM68yG+INJ2gvq8=
 github.com/haya14busa/go-sarif v0.0.0-20210102043135-e2c5fed2fa3d/go.mod h1:1Hkn3JseGMB/hv1ywzkapVQDWV3bFgp6POZobZmR/5g=
 github.com/haya14busa/secretbox v0.0.0-20180525171038-07c7ecf409f5 h1:ylgozezbuxA/i4uFtWCG/qGKYOZydsS8VUNNwfugn2Q=
 github.com/haya14busa/secretbox v0.0.0-20180525171038-07c7ecf409f5/go.mod h1:FGO/dXIFZnan7KvvUSFk1hYMnoVNzB6NTMPrmke8SSI=
diff --git a/parser/sarif.go b/parser/sarif.go
index 58be909f..ee147fa2 100644
--- a/parser/sarif.go
+++ b/parser/sarif.go
@@ -7,6 +7,7 @@ import (
 	"os"
 	"path/filepath"
 
+	"github.com/haya14busa/go-sarif/sarif"
 	"github.com/reviewdog/reviewdog/proto/rdf"
 	"github.com/reviewdog/reviewdog/service/serviceutil"
 )
@@ -22,7 +23,7 @@ func NewSarifParser() Parser {
 }
 
 func (p *SarifParser) Parse(r io.Reader) ([]*rdf.Diagnostic, error) {
-	slf := new(SarifJson)
+	slf := new(sarif.Sarif)
 	if err := json.NewDecoder(r).Decode(slf); err != nil {
 		return nil, err
 	}
@@ -38,9 +39,12 @@ func (p *SarifParser) Parse(r io.Reader) ([]*rdf.Diagnostic, error) {
 		tool := run.Tool
 		driver := tool.Driver
 		name := driver.Name
-		informationURI := driver.InformationURI
-		baseURIs := run.OriginalURIBaseIds
-		rules := map[string]SarifRule{}
+		informationURI := ""
+		if driver.InformationURI != nil {
+			informationURI = *driver.InformationURI
+		}
+		baseURIs := run.OriginalURIBaseIDS
+		rules := map[string]sarif.ReportingDescriptor{}
 		for _, rule := range driver.Rules {
 			rules[rule.ID] = rule
 		}
@@ -49,69 +53,86 @@ func (p *SarifParser) Parse(r io.Reader) ([]*rdf.Diagnostic, error) {
 			if err != nil {
 				return nil, err
 			}
-			message := result.Message.GetText()
-			ruleID := result.RuleID
-			rule := rules[ruleID]
-			level := result.Level
-			if level == "" {
-				level = rule.DefaultConfiguration.Level
+			message := getText(result.Message)
+			rule := sarif.ReportingDescriptor{}
+			ruleID := ""
+			if result.RuleID != nil {
+				ruleID = *result.RuleID
+			}
+			rule = rules[ruleID]
+			level := ""
+			if result.Level != nil {
+				level = string(*result.Level)
+			} else if rule.DefaultConfiguration != nil && rule.DefaultConfiguration.Level != nil {
+				level = string(*rule.DefaultConfiguration.Level)
 			}
 			suggestionsMap := map[string][]*rdf.Suggestion{}
 			for _, fix := range result.Fixes {
 				for _, artifactChange := range fix.ArtifactChanges {
 					suggestions := []*rdf.Suggestion{}
-					path, err := artifactChange.ArtifactLocation.GetPath(baseURIs, basedir)
+					path, err := getPath(artifactChange.ArtifactLocation, baseURIs, basedir)
 					if err != nil {
 						// invalid path
 						return nil, err
 					}
 					for _, replacement := range artifactChange.Replacements {
 						deletedRegion := replacement.DeletedRegion
-						rng := deletedRegion.GetRdfRange()
-						if rng == nil {
+						rng := getRdfRange(deletedRegion)
+						if rng == nil || replacement.InsertedContent.Text == nil {
 							// No line information in fix
 							continue
 						}
 						s := &rdf.Suggestion{
 							Range: rng,
-							Text:  replacement.InsertedContent.Text,
+							Text:  *replacement.InsertedContent.Text,
 						}
 						suggestions = append(suggestions, s)
 					}
 					suggestionsMap[path] = suggestions
 				}
 			}
-			for _, location := range result.Locations {
-				physicalLocation := location.PhysicalLocation
-				artifactLocation := physicalLocation.ArtifactLocation
-				path, err := artifactLocation.GetPath(baseURIs, basedir)
+
+			relatedLocs := []*rdf.RelatedLocation{}
+			for _, relLoc := range result.RelatedLocations {
+				loc, err := toRDFormatLocation(relLoc, baseURIs, basedir)
 				if err != nil {
-					// invalid path
 					return nil, err
 				}
-				region := physicalLocation.Region
-				rng := region.GetRdfRange()
+				l := &rdf.RelatedLocation{
+					Location: loc,
+				}
+				if relLoc.Message != nil {
+					l.Message = getText(*relLoc.Message)
+				}
+				relatedLocs = append(relatedLocs, l)
+			}
+
+			for _, location := range result.Locations {
 				var code *rdf.Code
 				if ruleID != "" {
 					code = &rdf.Code{
 						Value: ruleID,
-						Url:   rule.HelpURI,
 					}
+					if rule.HelpURI != nil {
+						code.Url = *rule.HelpURI
+					}
+				}
+				loc, err := toRDFormatLocation(location, baseURIs, basedir)
+				if err != nil {
+					return nil, err
 				}
 				d := &rdf.Diagnostic{
-					Message: message,
-					Location: &rdf.Location{
-						Path:  path,
-						Range: rng,
-					},
+					Message:  message,
+					Location: loc,
 					Severity: severity(level),
 					Source: &rdf.Source{
 						Name: name,
 						Url:  informationURI,
 					},
-					Code:           code,
-					Suggestions:    suggestionsMap[path],
-					OriginalOutput: string(original),
+					Code:             code,
+					Suggestions:      suggestionsMap[loc.GetPath()],
+					RelatedLocations: relatedLocs,
+					OriginalOutput:   string(original),
 				}
 				ds = append(ds, d)
 			}
@@ -120,65 +141,47 @@ func (p *SarifParser) Parse(r io.Reader) ([]*rdf.Diagnostic, error) {
 	return ds, nil
 }
 
-// SARIF JSON Format
-//
-// References:
-//   - https://sarifweb.azurewebsites.net/
-type SarifJson struct {
-	Runs []struct {
-		OriginalURIBaseIds map[string]SarifOriginalURI `json:"originalUriBaseIds"`
-		Results            []struct {
-			Level     string `json:"level,omitempty"`
-			Locations []struct {
-				PhysicalLocation struct {
-					ArtifactLocation SarifArtifactLocation `json:"artifactLocation,omitempty"`
-					Region           SarifRegion           `json:"region,omitempty"`
-				} `json:"physicalLocation,omitempty"`
-			} `json:"locations"`
-			Message SarifText `json:"message"`
-			RuleID  string    `json:"ruleId,omitempty"`
-			Fixes   []struct {
-				Description     SarifText `json:"description"`
-				ArtifactChanges []struct {
-					ArtifactLocation SarifArtifactLocation `json:"artifactLocation,omitempty"`
-					Replacements     []struct {
-						DeletedRegion   SarifRegion `json:"deletedRegion"`
-						InsertedContent struct {
-							Text string `json:"text"`
-						} `json:"insertedContent,omitempty"`
-					} `json:"replacements"`
-				} `json:"artifactChanges"`
-			} `json:"fixes,omitempty"`
-		} `json:"results"`
-		Tool struct {
-			Driver struct {
-				FullName       string      `json:"fullName"`
-				InformationURI string      `json:"informationUri"`
-				Name           string      `json:"name"`
-				Rules          []SarifRule `json:"rules"`
-			} `json:"driver"`
-		} `json:"tool"`
-	} `json:"runs"`
-}
-
-type SarifOriginalURI struct {
-	URI string `json:"uri"`
-}
-
-type SarifArtifactLocation struct {
-	URI       string `json:"uri,omitempty"`
-	URIBaseID string `json:"uriBaseId"`
-	Index     int    `json:"index,omitempty"`
+func toRDFormatLocation(location sarif.Location,
+	baseURIs map[string]sarif.ArtifactLocation,
+	basedir string,
+) (*rdf.Location, error) {
+	physicalLocation := location.PhysicalLocation
+	artifactLocation := physicalLocation.ArtifactLocation
+	loc := sarif.ArtifactLocation{}
+	if artifactLocation != nil {
+		loc = *artifactLocation
+	}
+	path, err := getPath(loc, baseURIs, basedir)
+	if err != nil {
+		// invalid path
+		return nil, err
+	}
+	region := sarif.Region{}
+	if physicalLocation.Region != nil {
+		region = *physicalLocation.Region
+	}
+	return &rdf.Location{
+		Path:  path,
+		Range: getRdfRange(region),
+	}, nil
 }
 
-func (l *SarifArtifactLocation) GetPath(
-	baseURIs map[string]SarifOriginalURI,
+func getPath(
+	l sarif.ArtifactLocation,
+	baseURIs map[string]sarif.ArtifactLocation,
 	basedir string,
 ) (string, error) {
-	uri := l.URI
-	baseURI := baseURIs[l.URIBaseID].URI
-	if baseURI != "" {
-		if u, err := url.JoinPath(baseURI, uri); err == nil {
+	uri := ""
+	if l.URI != nil {
+		uri = *l.URI
+	}
+	urlBaseID := ""
+	if l.URIBaseID != nil {
+		urlBaseID = *l.URIBaseID
+	}
+	baseURI := baseURIs[urlBaseID].URI
+	if baseURI != nil && *baseURI != "" {
+		if u, err := url.JoinPath(*baseURI, uri); err == nil {
 			uri = u
 		}
 	}
@@ -193,26 +196,17 @@ func (l *SarifArtifactLocation) GetPath(
 	return path, nil
 }
 
-type SarifText struct {
-	Text     string  `json:"text,omitempty"`
-	Markdown *string `json:"markdown,omitempty"`
-}
-
-func (t *SarifText) GetText() string {
-	text := t.Text
-	if t.Markdown != nil {
-		text = *t.Markdown
+func getText(msg sarif.Message) string {
+	text := ""
+	if msg.Text != nil {
+		text = *msg.Text
+	}
+	if msg.Markdown != nil {
+		text = *msg.Markdown
 	}
 	return text
 }
 
-type SarifRegion struct {
-	StartLine   *int `json:"startLine"`
-	StartColumn *int `json:"startColumn,omitempty"`
-	EndLine     *int `json:"endLine,omitempty"`
-	EndColumn   *int `json:"endColumn,omitempty"`
-}
-
 // convert SARIF Region to RDF Range
 //
 // * Supported SARIF: Line + Column Text region
@@ -277,13 +271,13 @@ type SarifRegion struct {
 // -> RDF: { "start": { "line": 1, "column": 1 } }
 //
 //	= { "start": { "line": 1, "column": 1 }, "end": { "line": 1, "column": 1 } }
-func (r *SarifRegion) GetRdfRange() *rdf.Range {
+func getRdfRange(r sarif.Region) *rdf.Range {
 	if r.StartLine == nil {
 		// No line information
 		return nil
 	}
 	startLine := *r.StartLine
-	startColumn := 1 // default value of startColumn in SARIF is 1
+	var startColumn int64 = 1 // default value of startColumn in SARIF is 1
 	if r.StartColumn != nil {
 		startColumn = *r.StartColumn
 	}
@@ -291,7 +285,7 @@ func (r *SarifRegion) GetRdfRange() *rdf.Range {
 	if r.EndLine != nil {
 		endLine = *r.EndLine
 	}
-	endColumn := 0 // default value of endColumn in SARIF is null (that means EOL)
+	var endColumn int64 = 0 // default value of endColumn in SARIF is null (that means EOL)
 	if r.EndColumn != nil {
 		endColumn = *r.EndColumn
 	}
@@ -336,16 +330,3 @@ func (r *SarifRegion) GetRdfRange() *rdf.Range {
 	}
 	return rng
 }
-
-type SarifRule struct {
-	ID                   string    `json:"id"`
-	Name                 string    `json:"name"`
-	ShortDescription     SarifText `json:"shortDescription"`
-	FullDescription      SarifText `json:"fullDescription"`
-	Help                 SarifText `json:"help"`
-	HelpURI              string    `json:"helpUri"`
-	DefaultConfiguration struct {
-		Level string `json:"level"`
-		Rank  int    `json:"rank"`
-	} `json:"defaultConfiguration"`
-}


Test patch:
diff --git a/parser/sarif_test.go b/parser/sarif_test.go
index 3514c4de..4d90e5ac 100644
--- a/parser/sarif_test.go
+++ b/parser/sarif_test.go
@@ -134,7 +134,9 @@ var fixtures = [][]string{{
 		{
 			"originalUriBaseIds": {
 				"SRCROOT": {
-					"description": "uri deleted root"
+					"description": {
+						"text": "uri deleted root"
+					}
 				}
 			},
 			"results": [
@@ -354,5 +356,84 @@ var fixtures = [][]string{{
 		"value": "CVE-2018-14618/curl",
 		"url": "https://avd.aquasec.com/nvd/cve-2018-14618"
 	}
+}`},
+	{fmt.Sprintf(`{
+	"runs": [ {
+		"originalUriBaseIds": {
+			"ROOTPATH": {
+			  "uri": "%s"
+			}
+		},
+		"tool": {
+			"driver": {
+				"name": "driver_name"
+			}
+		},
+		"results": [
+			{
+				"ruleId": "PY2335",
+				"message": {
+          "text": "Use of tainted variable 'expr' in the insecure function 'eval'."
+        },
+				"locations": [
+					{
+						"physicalLocation": {
+							"artifactLocation": {
+								"uri": "3-Beyond-basics/bad-eval.py"
+							},
+							"region": {
+								"startLine": 4
+							}
+						}
+					}
+				],
+				"relatedLocations": [
+					{
+						"message": {
+							"text": "The tainted data entered the system here."
+						},
+						"physicalLocation": {
+							"artifactLocation": {
+								"uri": "3-Beyond-basics/bad-eval.py"
+							},
+							"region": {
+								"startLine": 3
+							}
+						}
+					}
+				]
+			}
+			]
+	  } ]
+  }
+`, basedir()), `{
+	"message": "Use of tainted variable 'expr' in the insecure function 'eval'.",
+	"location": {
+		"path": "3-Beyond-basics/bad-eval.py",
+    "range": {
+      "start": {
+        "line": 4
+      }
+    }
+	},
+	"source": {
+		"name": "driver_name"
+	},
+	"code": {
+		"value": "PY2335"
+	},
+  "relatedLocations": [
+    {
+      "message": "The tainted data entered the system here.",
+			"location": {
+				"path": "3-Beyond-basics/bad-eval.py",
+				"range": {
+					"start": {
+						"line": 3
+					}
+				}
+			}
+    }
+  ]
 }`},
 }