diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml index a20265cc..0c549f90 100644 --- a/.github/workflows/reviewdog.yml +++ b/.github/workflows/reviewdog.yml @@ -233,6 +233,34 @@ jobs: cat ./_testdata/custom_rdjson.json | \ reviewdog -name="custom-rdjson" -f=rdjson -reporter=github-pr-annotations + reviewdog-sarif: + permissions: + contents: read + name: reviewdog (sarif) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: "go.mod" + - name: Setup reviewdog + # uses: reviewdog/action-setup@v1 + run: | + go install ./cmd/reviewdog + - name: Custom rdjson test + run: | + mkdir ../results + cat ./_testdata/custom_rdjson.json | + reviewdog -name="custom-rdjson" -f=rdjson -reporter=sarif | + tee ../results/custom-rdjson.sarif + - name: Install linters + run: go install golang.org/x/lint/golint@latest + - name: Run golint + run: | + golint ./... | reviewdog -f=golint -reporter=sarif | + tee ../results/golint.sarif + - uses: github/codeql-action/upload-sarif@v3 + typos: name: runner / typos runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 010a9046..e0adc1f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 outdated comments in github-pr-review reporter. Note that it won't delete comments if there is a reply considering there can be a meaningful discussion. -- [#1806](https://github.com/reviewdog/reviewdog/pull/1806) Add -reporter=rdjson/rdjsonl which outputs rdjson/rdjsonl format to stdout. It also changes the default behavior of -diff and -filter-mode for local reporters. If -filter-mode is not provided (-filter-mode=default) and -diff flag is not provided, reviewdog automatically set -filter-mode=nofilter. +- [#1806](https://github.com/reviewdog/reviewdog/pull/1806) Add + -reporter=rdjson/rdjsonl which outputs rdjson/rdjsonl format to stdout. It + also changes the default behavior of -diff and -filter-mode for local + reporters. If -filter-mode is not provided (-filter-mode=default) and -diff + flag is not provided, reviewdog automatically set -filter-mode=nofilter. +- [#1807](https://github.com/reviewdog/reviewdog/pull/1807) Add -reporter=sarif + which outputs SARIF format to stdout. You can upload the output SARIF to + GitHub and see code scanning alerts. + ### :bug: Fixes - ... diff --git a/cmd/reviewdog/main.go b/cmd/reviewdog/main.go index c3509f11..d5c2a5a8 100644 --- a/cmd/reviewdog/main.go +++ b/cmd/reviewdog/main.go @@ -100,6 +100,9 @@ const ( "rdjsonl" Report results to stdout in rdjsonl format. + "sarif" + Report results to stdout in SARIF format. + "github-check" Report results to GitHub Check. It works both for Pull Requests and commits. For Pull Request, you can see report results in GitHub PullRequest Check @@ -452,6 +455,13 @@ func run(r io.Reader, w io.Writer, opt *option) error { } ds = d cs = reviewdog.NewRDJSONLCommentWriter(w) + case "sarif": + d, err := localDiffService(opt) + if err != nil { + return err + } + ds = d + cs = reviewdog.NewSARIFCommentWriter(w, toolName(opt)) } if isProject { diff --git a/comment_iowriter.go b/comment_iowriter.go index 9e043068..e69330cf 100644 --- a/comment_iowriter.go +++ b/comment_iowriter.go @@ -2,9 +2,11 @@ package reviewdog import ( "context" + "encoding/json" "fmt" "io" + "github.com/haya14busa/go-sarif/sarif" "github.com/reviewdog/reviewdog/proto/rdf" "google.golang.org/protobuf/encoding/protojson" ) @@ -142,3 +144,138 @@ func (cw *RDJSONCommentWriter) Flush(_ context.Context) error { } return nil } + +var _ CommentService = &SARIFCommentWriter{} + +// SARIFCommentWriter +type SARIFCommentWriter struct { + w io.Writer + comments []*Comment + toolName string +} + +func NewSARIFCommentWriter(w io.Writer, toolName string) *SARIFCommentWriter { + return &SARIFCommentWriter{w: w, toolName: toolName} +} + +func (cw *SARIFCommentWriter) Post(_ context.Context, c *Comment) error { + cw.comments = append(cw.comments, c) + return nil +} + +func (cw *SARIFCommentWriter) Flush(_ context.Context) error { + run := sarif.Run{ + Tool: sarif.Tool{ + Driver: sarif.ToolComponent{ + Name: cw.toolName, + Rules: make([]sarif.ReportingDescriptor, 0), + }, + }, + } + seenRules := make(map[string]bool) + for _, c := range cw.comments { + result := sarif.Result{ + Message: sarif.Message{ + Text: sarif.String(c.Result.Diagnostic.Message), + }, + } + if code := c.Result.Diagnostic.GetCode(); code.GetValue() != "" { + result.RuleID = sarif.String(code.GetValue()) + if seen := seenRules[code.GetValue()]; !seen { + seenRules[code.GetValue()] = true + rd := sarif.ReportingDescriptor{ + ID: code.GetValue(), + } + if code.GetUrl() != "" { + rd.HelpURI = sarif.String(code.GetUrl()) + } + run.Tool.Driver.Rules = append(run.Tool.Driver.Rules, rd) + } + } + level := severity2level(c.Result.Diagnostic.GetSeverity()) + if level != sarif.None { + result.Level = &level + } + artifactLoc := sarif.ArtifactLocation{ + URI: sarif.String(c.Result.Diagnostic.GetLocation().GetPath()), + } + result.Locations = []sarif.Location{{ + PhysicalLocation: &sarif.PhysicalLocation{ + ArtifactLocation: &artifactLoc, + Region: range2region(c.Result.Diagnostic.GetLocation().GetRange()), + }, + }} + if len(c.Result.Diagnostic.GetSuggestions()) > 0 { + result.Fixes = make([]sarif.Fix, 0) + for _, suggestion := range c.Result.Diagnostic.GetSuggestions() { + result.Fixes = append(result.Fixes, sarif.Fix{ + ArtifactChanges: []sarif.ArtifactChange{ + { + ArtifactLocation: artifactLoc, + Replacements: []sarif.Replacement{{ + DeletedRegion: *range2region(suggestion.GetRange()), + InsertedContent: &sarif.ArtifactContent{ + Text: sarif.String(suggestion.GetText()), + }, + }}, + }, + }, + }) + } + } + if len(c.Result.Diagnostic.GetRelatedLocations()) > 0 { + result.RelatedLocations = make([]sarif.Location, 0) + for _, relLoc := range c.Result.Diagnostic.GetRelatedLocations() { + result.RelatedLocations = append(result.RelatedLocations, sarif.Location{ + PhysicalLocation: &sarif.PhysicalLocation{ + ArtifactLocation: &sarif.ArtifactLocation{ + URI: sarif.String(relLoc.GetLocation().GetPath()), + }, + Region: range2region(relLoc.GetLocation().GetRange()), + }, + Message: &sarif.Message{ + Text: sarif.String(relLoc.Message), + }, + }) + } + } + run.Results = append(run.Results, result) + } + slf := sarif.NewSarif() + slf.Runs = []sarif.Run{run} + encoder := json.NewEncoder(cw.w) + encoder.SetIndent("", " ") + return encoder.Encode(slf) +} + +func range2region(rng *rdf.Range) *sarif.Region { + region := &sarif.Region{} + start := rng.GetStart() + end := rng.GetEnd() + if start.GetLine() > 0 { + region.StartLine = sarif.Int64(int64(start.GetLine())) + } + if start.GetColumn() > 0 { + // Column is not usually unicodeCodePoints, but let's just keep it + // as is... + region.StartColumn = sarif.Int64(int64(start.GetColumn())) + } + if end.GetLine() > 0 { + region.EndLine = sarif.Int64(int64(end.GetLine())) + } + if end.GetColumn() > 0 { + region.EndColumn = sarif.Int64(int64(end.GetColumn())) + } + return region +} + +func severity2level(s rdf.Severity) sarif.Level { + switch s { + case rdf.Severity_ERROR: + return sarif.Error + case rdf.Severity_WARNING: + return sarif.Warning + default: + return sarif.None + } +} diff --git a/comment_iowriter_test.go b/comment_iowriter_test.go index 1ab06fde..8aeaf502 100644 --- a/comment_iowriter_test.go +++ b/comment_iowriter_test.go @@ -259,3 +259,126 @@ func TestRDJSONCommentWriter_Post(t *testing.T) { t.Errorf("got\n%v\nwant:\n%v", got, want) } } + +func TestSARIFCommentWriter_Post(t *testing.T) { + comments := []*Comment{ + { + Result: &filter.FilteredDiagnostic{ + Diagnostic: &rdf.Diagnostic{ + Location: &rdf.Location{Path: "/path/to/file"}, + Message: "message", + }, + }, + ToolName: "tool name", + }, + { + Result: &filter.FilteredDiagnostic{ + Diagnostic: &rdf.Diagnostic{ + Location: &rdf.Location{ + Path: "/path/to/file", + Range: &rdf.Range{Start: &rdf.Position{ + Column: 14, + }}, + }, + Message: "message", + }, + }, + }, + { + Result: &filter.FilteredDiagnostic{ + Diagnostic: &rdf.Diagnostic{ + Location: &rdf.Location{ + Path: "/path/to/file", + Range: &rdf.Range{Start: &rdf.Position{ + Column: 14, + }}, + }, + Message: "message", + Source: &rdf.Source{ + Name: "tool name in Diagnostic", + Url: "tool url", + }, + }, + }, + }, + } + buf := new(bytes.Buffer) + cw := NewSARIFCommentWriter(buf, "tool name [constructor]") + for _, c := range comments { + if err := cw.Post(context.Background(), c); err != nil { + t.Error(err) + } + } + if err := cw.Flush(context.Background()); err != nil { + t.Error(err) + } + want := ` +{ + "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json", + "runs": [ + { + "results": [ + { + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "/path/to/file" + }, + "region": {} + } + } + ], + "message": { + "text": "message" + } + }, + { + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "/path/to/file" + }, + "region": { + "startColumn": 14 + } + } + } + ], + "message": { + "text": "message" + } + }, + { + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "/path/to/file" + }, + "region": { + "startColumn": 14 + } + } + } + ], + "message": { + "text": "message" + } + } + ], + "tool": { + "driver": { + "name": "tool name [constructor]" + } + } + } + ], + "version": "2.1.0" +}` + got := strings.TrimSpace(buf.String()) + if got != strings.TrimSpace(want) { + t.Errorf("got\n%v\nwant:\n%v", got, want) + } +}