Skip to content

Commit

Permalink
Add SonarQube output support (#123)
Browse files Browse the repository at this point in the history
  • Loading branch information
cognitivegears authored Aug 11, 2021
1 parent ec05f53 commit 6126a51
Show file tree
Hide file tree
Showing 21 changed files with 577 additions and 55 deletions.
86 changes: 82 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ Companies like [GitHub](https://github.com/github/renaming), [Twitter](https://t
- [Docker](#docker)
- [Usage](#usage)
- [File globs](#file-globs)
- [stdin](#stdin)
- [STDIN](#stdin)
- [Output](#output)
- [Rules](#rules)
- [Options](#options)
- [Disabling Default Rules](#disabling-default-rules)
Expand Down Expand Up @@ -152,7 +153,7 @@ Flags:
--exit-1-on-failure Exit with exit code 1 on failures
-h, --help help for woke
--no-ignore Ignored files in .gitignore, .ignore, .wokeignore, .git/info/exclude, and inline ignores are processed
-o, --output string Output type [text,simple,github-actions,json] (default "text")
-o, --output string Output type [text,simple,github-actions,json,sonarqube] (default "text")
--stdin Read from stdin
-v, --version version for woke
```
Expand Down Expand Up @@ -180,9 +181,9 @@ test.txt:5:2-11: `blacklist` may be insensitive, use `denylist`, `blocklist` ins
^
```

### stdin
### STDIN

You can also provide text to `woke` via stdin
You can also provide text to `woke` via STDIN (Standard Input)

```bash
$ echo "This has whitelist from stdin" | woke --stdin
Expand All @@ -191,6 +192,83 @@ This has whitelist from stdin
^
```

This option may not be used at the same time as [File Globs](#file-globs)

### Output

Options for output include text (default), simple, json, github-actions, or sonarqube format. The following fields are supported, depending on format:

| Field | Description |
| ------------ | ------------ |
| filepath | Relative path to file including filename |
| rulename | Name of the rule from the config file |
| termname | Specific term that was found in the text |
| alternative | List of alternative terms to use instead |
| note | Note about reasoning for inclusion |
| severity | From config, one of "error", "warning", or "info" |
| optionbool | Option value, true or false |
| linecontents | Contents of the line with finding |
| lineno | Line number, 1 based |
| startcol | Starting column number, 0 based |
| endcol | Ending column number, 0 based |
| description | Description of finding |

Output is sent to STDOUT (Standard Output), which may be redirected to a file to save the results of a scan.

#### text

text is the default output format for woke. Displays each result on two lines. Includes color formatting if the terminal supports it.

Structure:

```
<filepath>:<lineno>:<startcol>-<endcol>: <description> (<severity>)
<linecontents>
```

#### simple

simple is a format similar to text, but without color support and with each result on a single line.

Structure:

```
<filepath>:<lineno>:<startcol>: <description>
```

#### github-actions

The github-actions output type is intended for integration with [GitHub Actions](https://github.com/features/actions). See [woke-action](https://github.com/get-woke/woke-action) for more information on integration.

Structure:

```
::<severity> file=<filepath>,line=<lineno>,col=<startcol>::<description>
```

#### json

Outputs the results as a series of [json](https://www.json.org/json-en.html) formatted structures, one per line. In order to parse as a JSON document, each line must be processed separately. This output type includes every field available in woke.

Structure:

```
{"Filename":"<filepath>","Results":[{"Rule":{"Name":"<rulename>","Terms":["<termname>",...],"Alternatives":["<alternative>",...],"Note":"<note>","Severity":"<severity>","Options":{"WordBoundary":<optionbool>,"WordBoundaryStart":<optionbool>,"WordBoundaryEnd":<optionbool>,"IncludeNote":<optionbool>}},"Finding":"<termname>","Line":"<linecontents>","StartPosition":{"Filename":"<filepath>","Offset":0,"Line":<lineno>,"Column":<startcol>},"EndPosition":{"Filename":"<filepath>","Offset":0,"Line":<lineno>,"Column":<endcol>},"Reason":"<description>"}]}
```

#### sonarqube

Format used to populate results into the popular [SonarQube](https://www.sonarqube.org/) code quality tool. Note: woke is not executed as part of SonarQube itself, so must be run and the results file output prior to execution. Typically woke would be executed with an automation server such as Jenkins, Travis CI or Github Actions prior to creating the SonarQube report. For details on the file format, see [Generic Issue Input Format](https://docs.sonarqube.org/latest/analysis/generic-issue/). The [Analysis Parameter](https://docs.sonarqube.org/latest/analysis/analysis-parameters/) `sonar.externalIssuesReportPaths` is used to point to the path to the report file generated by woke.

Structure:

```
{"issues":[{"engineId":"woke","ruleId":"<rulename>","primaryLocation":{"message":"<description>","filePath":"<filepath>","textRange":{"startLine":<lineno>,"startColumn":<startcol>,"endColumn":<endcol>}},"type":"CODE_SMELL","severity":"<sonarqubeseverity>"}
]}
```

_Note: sonarqubeseverity is mapped from severity, such that an error in woke is translated to a MAJOR, warning to a MINOR, and info to INFO_

### Rules

Configure your custom rules config in `.woke.yaml` or `.woke.yml`. `woke` uses the following precedence order. Each item takes precedence over the item below it:
Expand Down
4 changes: 2 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func rootRunE(cmd *cobra.Command, args []string) error {

p := parser.NewParser(cfg.Rules, ignorer)

print, err := printer.NewPrinter(outputName)
print, err := printer.NewPrinter(outputName, output.Stdout)
if err != nil {
return err
}
Expand All @@ -112,7 +112,7 @@ func rootRunE(cmd *cobra.Command, args []string) error {
}

if findings == 0 {
if cfg.GetSuccessExitMessage() != "" {
if print.PrintSuccessExitMessage() && cfg.GetSuccessExitMessage() != "" {
fmt.Fprintln(output.Stdout, cfg.GetSuccessExitMessage())
}
}
Expand Down
8 changes: 5 additions & 3 deletions pkg/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"sync"

"github.com/get-woke/woke/pkg/ignore"
"github.com/get-woke/woke/pkg/output"
"github.com/get-woke/woke/pkg/printer"
"github.com/get-woke/woke/pkg/result"
"github.com/get-woke/woke/pkg/rule"
Expand Down Expand Up @@ -43,11 +42,14 @@ func NewParser(rules []*rule.Rule, ignorer *ignore.Ignore) *Parser {

// ParsePaths parses all files provided and returns the number of files with findings
func (p *Parser) ParsePaths(print printer.Printer, paths ...string) int {
print.Start()
defer print.End()

// data provided through stdin
if util.InSlice(os.Stdin.Name(), paths) {
r, _ := p.generateFileFindings(os.Stdin)
if r.Len() > 0 {
print.Print(output.Stdout, r)
print.Print(r)
}
return r.Len()
}
Expand Down Expand Up @@ -76,7 +78,7 @@ func (p *Parser) ParsePaths(print printer.Printer, paths ...string) int {
findings := 0
for r := range p.rchan {
sort.Sort(r)
print.Print(output.Stdout, &r)
print.Print(&r)
findings++
}
return findings
Expand Down
13 changes: 11 additions & 2 deletions pkg/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package parser

import (
"go/token"
"io"
"io/ioutil"
"os"
"path/filepath"
Expand All @@ -21,11 +20,21 @@ type testPrinter struct {
}

// Print doesn't actually write anything, just stores the results in memory so they can be read later
func (p *testPrinter) Print(_ io.Writer, r *result.FileResults) error {
func (p *testPrinter) Print(r *result.FileResults) error {
p.results = append(p.results, r)
return nil
}

func (p *testPrinter) Start() {
}

func (p *testPrinter) End() {
}

func (p *testPrinter) PrintSuccessExitMessage() bool {
return true
}

func testParser() *Parser {
r := rule.TestRule
return NewParser([]*rule.Rule{&r}, ignore.NewIgnore([]string{}))
Expand Down
22 changes: 17 additions & 5 deletions pkg/printer/githubactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,34 @@ import (
)

// GitHubActions is a GitHubActions printer meant for use by a GitHub Action annotation
type GitHubActions struct{}
type GitHubActions struct {
writer io.Writer
}

// NewGitHubActions returns a new GitHubActions printer
func NewGitHubActions() *GitHubActions {
return &GitHubActions{}
func NewGitHubActions(w io.Writer) *GitHubActions {
return &GitHubActions{writer: w}
}

func (p *GitHubActions) PrintSuccessExitMessage() bool {
return true
}

// Print prints in the format for GitHub actions
// https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message
func (p *GitHubActions) Print(w io.Writer, fs *result.FileResults) error {
func (p *GitHubActions) Print(fs *result.FileResults) error {
for _, r := range fs.Results {
fmt.Fprintln(w, formatResultForGitHubAction(r))
fmt.Fprintln(p.writer, formatResultForGitHubAction(r))
}
return nil
}

func (p *GitHubActions) Start() {
}

func (p *GitHubActions) End() {
}

func formatResultForGitHubAction(r result.Result) string {
return fmt.Sprintf("::%s file=%s,line=%d,col=%d::%s",
translateSeverityForAction(r.GetSeverity()),
Expand Down
28 changes: 25 additions & 3 deletions pkg/printer/githubactions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,34 @@ func TestTranslateSeverityForAction(t *testing.T) {
assert.Equal(t, translateSeverityForAction(rule.SevInfo), "warning")
}

func TestGitHubActions_PrintSuccessExitMessage(t *testing.T) {
buf := new(bytes.Buffer)
p := NewGitHubActions(buf)
assert.Equal(t, true, p.PrintSuccessExitMessage())
}

func TestGitHubActions_Print(t *testing.T) {
p := NewGitHubActions()
res := generateFileResult()
buf := new(bytes.Buffer)
assert.NoError(t, p.Print(buf, res))
p := NewGitHubActions(buf)
res := generateFileResult()
assert.NoError(t, p.Print(res))
got := buf.String()
expected := fmt.Sprintf("::warning file=foo.txt,line=1,col=6::%s\n", res.Results[0].Reason())
assert.Equal(t, expected, got)
}

func TestGitHubActions_Start(t *testing.T) {
buf := new(bytes.Buffer)
p := NewGitHubActions(buf)
p.Start()
got := buf.String()
assert.Equal(t, ``, got)
}

func TestGitHubActions_End(t *testing.T) {
buf := new(bytes.Buffer)
p := NewGitHubActions(buf)
p.End()
got := buf.String()
assert.Equal(t, ``, got)
}
22 changes: 17 additions & 5 deletions pkg/printer/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,32 @@ import (
)

// JSON is a JSON printer meant for a machine to read
type JSON struct{}
type JSON struct {
writer io.Writer
}

// NewJSON returns a new JSON printer
func NewJSON() *JSON {
return &JSON{}
func NewJSON(w io.Writer) *JSON {
return &JSON{writer: w}
}

func (p *JSON) PrintSuccessExitMessage() bool {
return true
}

func (p *JSON) Start() {
}

func (p *JSON) End() {
}

// Print prints in FileResults as json
// NOTE: The JSON printer will bring each line result as a JSON string.
// It will not be presented as an array of FileResults. You will neeed to
// Split by new line to parse the full output
func (p *JSON) Print(w io.Writer, fs *result.FileResults) error {
func (p *JSON) Print(fs *result.FileResults) error {
var buf bytes.Buffer
err := json.NewEncoder(&buf).Encode(fs)
fmt.Fprint(w, buf.String()) // json Encoder already puts a new line in, so no need for Println here
fmt.Fprint(p.writer, buf.String()) // json Encoder already puts a new line in, so no need for Println here
return err
}
51 changes: 47 additions & 4 deletions pkg/printer/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,56 @@ import (
"github.com/stretchr/testify/assert"
)

func TestJSON_Print(t *testing.T) {
p := NewJSON()
func TestJSON_Print_JSON(t *testing.T) {
buf := new(bytes.Buffer)
res := generateFileResult()
p := NewJSON(buf)
assert.NoError(t, p.Print(res))
expected := "{\"Filename\":\"foo.txt\",\"Results\":[{\"Rule\":{\"Name\":\"whitelist\",\"Terms\":[\"whitelist\",\"white-list\",\"whitelisted\",\"white-listed\"],\"Alternatives\":[\"allowlist\"],\"Note\":\"\",\"Severity\":\"warning\",\"Options\":{\"WordBoundary\":false,\"WordBoundaryStart\":false,\"WordBoundaryEnd\":false,\"IncludeNote\":null}},\"Finding\":\"whitelist\",\"Line\":\"this whitelist must change\",\"StartPosition\":{\"Filename\":\"foo.txt\",\"Offset\":0,\"Line\":1,\"Column\":6},\"EndPosition\":{\"Filename\":\"foo.txt\",\"Offset\":0,\"Line\":1,\"Column\":15},\"Reason\":\"`whitelist` may be insensitive, use `allowlist` instead\"}]}\n"
got := buf.String()
assert.Equal(t, expected, got)
}

func TestJSON_PrintSuccessExitMessage(t *testing.T) {
buf := new(bytes.Buffer)

p := NewJSON(buf)
assert.Equal(t, true, p.PrintSuccessExitMessage())
}

func TestJSON_Start(t *testing.T) {
buf := new(bytes.Buffer)
p := NewJSON(buf)
p.Start()
got := buf.String()

expected := ``
assert.Equal(t, expected, got)
}

func TestJSON_End(t *testing.T) {
buf := new(bytes.Buffer)
assert.NoError(t, p.Print(buf, res))
p := NewJSON(buf)
p.End()
got := buf.String()

expected := ``
assert.Equal(t, expected, got)
}

func TestJSON_Multiple(t *testing.T) {
buf := new(bytes.Buffer)
p := NewJSON(buf)
p.Start()
res := generateFileResult()
assert.NoError(t, p.Print(res))
res = generateSecondFileResult()
assert.NoError(t, p.Print(res))
res = generateThirdFileResult()
assert.NoError(t, p.Print(res))
p.End()
got := buf.String()

expected := `{"Filename":"foo.txt","Results":[{"Rule":{"Name":"whitelist","Terms":["whitelist","white-list","whitelisted","white-listed"],"Alternatives":["allowlist"],"Note":"","Severity":"warning","Options":{"WordBoundary":false,"WordBoundaryStart":false,"WordBoundaryEnd":false,"IncludeNote":null}},"Finding":"whitelist","Line":"this whitelist must change","StartPosition":{"Filename":"foo.txt","Offset":0,"Line":1,"Column":6},"EndPosition":{"Filename":"foo.txt","Offset":0,"Line":1,"Column":15},"Reason":"` + "`whitelist`" + ` may be insensitive, use ` + "`allowlist`" + ` instead"}]}` + "\n"
expected := "{\"Filename\":\"foo.txt\",\"Results\":[{\"Rule\":{\"Name\":\"whitelist\",\"Terms\":[\"whitelist\",\"white-list\",\"whitelisted\",\"white-listed\"],\"Alternatives\":[\"allowlist\"],\"Note\":\"\",\"Severity\":\"warning\",\"Options\":{\"WordBoundary\":false,\"WordBoundaryStart\":false,\"WordBoundaryEnd\":false,\"IncludeNote\":null}},\"Finding\":\"whitelist\",\"Line\":\"this whitelist must change\",\"StartPosition\":{\"Filename\":\"foo.txt\",\"Offset\":0,\"Line\":1,\"Column\":6},\"EndPosition\":{\"Filename\":\"foo.txt\",\"Offset\":0,\"Line\":1,\"Column\":15},\"Reason\":\"`whitelist` may be insensitive, use `allowlist` instead\"}]}\n{\"Filename\":\"bar.txt\",\"Results\":[{\"Rule\":{\"Name\":\"slave\",\"Terms\":[\"slave\"],\"Alternatives\":[\"follower\"],\"Note\":\"\",\"Severity\":\"error\",\"Options\":{\"WordBoundary\":false,\"WordBoundaryStart\":false,\"WordBoundaryEnd\":false,\"IncludeNote\":null}},\"Finding\":\"slave\",\"Line\":\"this slave term must change\",\"StartPosition\":{\"Filename\":\"bar.txt\",\"Offset\":0,\"Line\":1,\"Column\":6},\"EndPosition\":{\"Filename\":\"bar.txt\",\"Offset\":0,\"Line\":1,\"Column\":15},\"Reason\":\"`slave` may be insensitive, use `follower` instead\"}]}\n{\"Filename\":\"barfoo.txt\",\"Results\":[{\"Rule\":{\"Name\":\"test\",\"Terms\":[\"test\"],\"Alternatives\":[\"alternative\"],\"Note\":\"\",\"Severity\":\"info\",\"Options\":{\"WordBoundary\":false,\"WordBoundaryStart\":false,\"WordBoundaryEnd\":false,\"IncludeNote\":null}},\"Finding\":\"test\",\"Line\":\"this test must change\",\"StartPosition\":{\"Filename\":\"barfoo.txt\",\"Offset\":0,\"Line\":1,\"Column\":6},\"EndPosition\":{\"Filename\":\"barfoo.txt\",\"Offset\":0,\"Line\":1,\"Column\":15},\"Reason\":\"`test` may be insensitive, use `alternative` instead\"}]}\n"
assert.Equal(t, expected, got)
}
Loading

0 comments on commit 6126a51

Please sign in to comment.