Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SARIF as a reporter option #166

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions cmd/validator/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ optional flags:
-output
Destination of a file to outputting results
-reporter string
Format of the printed report. Options are standard and json (default "standard")
Format of the printed report. Options are standard, json, junit and sarif (default "standard")
-version
Version prints the release version of validator
*/
Expand Down Expand Up @@ -76,7 +76,7 @@ func getFlags() (validatorConfig, error) {
excludeDirsPtr := flag.String("exclude-dirs", "", "Subdirectories to exclude when searching for configuration files")
excludeFileTypesPtr := flag.String("exclude-file-types", "", "A comma separated list of file types to ignore")
outputPtr := flag.String("output", "", "Destination to a file to output results")
reportTypePtr := flag.String("reporter", "standard", "Format of the printed report. Options are standard and json")
reportTypePtr := flag.String("reporter", "standard", "Format of the printed report. Options are standard, json, junit and sarif")
versionPtr := flag.Bool("version", false, "Version prints the release version of validator")
groupOutputPtr := flag.String("groupby", "", "Group output by filetype, directory, pass-fail. Supported for Standard and JSON reports")
quietPrt := flag.Bool("quiet", false, "If quiet flag is set. It doesn't print any output to stdout.")
Expand All @@ -93,10 +93,12 @@ func getFlags() (validatorConfig, error) {
searchPaths = append(searchPaths, flag.Args()...)
}

if *reportTypePtr != "standard" && *reportTypePtr != "json" && *reportTypePtr != "junit" {
fmt.Println("Wrong parameter value for reporter, only supports standard, json or junit")
acceptedReportTypes := map[string]bool{"standard": true, "json": true, "junit": true, "sarif": true}

if !acceptedReportTypes[*reportTypePtr] {
fmt.Println("Wrong parameter value for reporter, only supports standard, json, junit or sarif")
flag.Usage()
return validatorConfig{}, errors.New("Wrong parameter value for reporter, only supports standard, json or junit")
return validatorConfig{}, errors.New("Wrong parameter value for reporter, only supports standard, json, junit or sarif")
}

if *reportTypePtr == "junit" && *groupOutputPtr != "" {
Expand All @@ -105,6 +107,12 @@ func getFlags() (validatorConfig, error) {
return validatorConfig{}, errors.New("Wrong parameter value for reporter, groupby is not supported for JUnit reports")
}

if *reportTypePtr == "sarif" && *groupOutputPtr != "" {
fmt.Println("Wrong parameter value for reporter, groupby is not supported for SARIF reports")
flag.Usage()
return validatorConfig{}, errors.New("Wrong parameter value for reporter, groupby is not supported for SARIF reports")
}

if depthPtr != nil && isFlagSet("depth") && *depthPtr < 0 {
fmt.Println("Wrong parameter value for depth, value cannot be negative.")
flag.Usage()
Expand Down Expand Up @@ -169,6 +177,8 @@ func getReporter(reportType, outputDest *string) reporter.Reporter {
return reporter.NewJunitReporter(*outputDest)
case "json":
return reporter.NewJSONReporter(*outputDest)
case "sarif":
return reporter.NewSARIFReporter(*outputDest)
default:
return reporter.StdoutReporter{}
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/validator/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ func Test_flags(t *testing.T) {
{"depth set", []string{"-depth=1", "."}, 0},
{"flags set, wrong reporter", []string{"--exclude-dirs=subdir", "--reporter=wrong", "."}, 1},
{"flags set, json reporter", []string{"--exclude-dirs=subdir", "--reporter=json", "."}, 0},
{"flags set, junit reported", []string{"--exclude-dirs=subdir", "--reporter=junit", "."}, 0},
{"flags set, junit reporter", []string{"--exclude-dirs=subdir", "--reporter=junit", "."}, 0},
{"flags set, sarif reporter", []string{"--exclude-dirs=subdir", "--reporter=sarif", "."}, 0},
{"bad path", []string{"/path/does/not/exit"}, 1},
{"exclude file types set", []string{"--exclude-file-types=json", "."}, 0},
{"multiple paths", []string{"../../test/fixtures/subdir/good.json", "../../test/fixtures/good.json"}, 0},
Expand Down
140 changes: 140 additions & 0 deletions pkg/reporter/reporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,40 @@ func Test_junitReport(t *testing.T) {
}
}

func Test_sarifReport(t *testing.T) {
reportNoValidationError := Report{
"good.xml",
"/fake/path/good.xml",
true,
nil,
false,
}

reportWithBackslashPath := Report{
"good.xml",
"\\fake\\path\\good.xml",
true,
nil,
false,
}

reportWithValidationError := Report{
"bad.xml",
"/fake/path/bad.xml",
false,
errors.New("Unable to parse bad.xml file"),
false,
}

reports := []Report{reportNoValidationError, reportWithValidationError, reportWithBackslashPath}

sarifReporter := SARIFReporter{}
err := sarifReporter.Print(reports)
if err != nil {
t.Errorf("Reporting failed")
}
}

func Test_jsonReporterWriter(t *testing.T) {
report := Report{
"good.json",
Expand Down Expand Up @@ -251,6 +285,112 @@ func Test_jsonReporterWriter(t *testing.T) {
}
}

func Test_sarifReporterWriter(t *testing.T) {
report := Report{
"good.json",
"test/output/example/good.json",
true,
nil,
false,
}
deleteFiles(t)

bytes, err := os.ReadFile("../../test/output/example/result.sarif")
require.NoError(t, err)

type args struct {
reports []Report
outputDest string
}
type want struct {
fileName string
data []byte
err assert.ErrorAssertionFunc
}

tests := map[string]struct {
args args
want want
}{
"normal/existing dir/default name": {
args: args{
reports: []Report{
report,
},
outputDest: "../../test/output",
},
want: want{
fileName: "result.sarif",
data: bytes,
err: assert.NoError,
},
},
"normal/file name is given": {
args: args{
reports: []Report{
report,
},
outputDest: "../../test/output/validator_result.sarif",
},
want: want{
fileName: "validator_result.sarif",
data: bytes,
err: assert.NoError,
},
},
"quash normal/empty string": {
args: args{
reports: []Report{
report,
},
outputDest: "",
},
want: want{
fileName: "",
data: nil,
err: assert.NoError,
},
},
"abnormal/non-existing dir": {
args: args{
reports: []Report{
report,
},
outputDest: "../../test/wrong/output",
},
want: want{
fileName: "",
data: nil,
err: assertRegexpError("failed to create a file: "),
},
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
sut := NewSARIFReporter(tt.args.outputDest)
err := sut.Print(tt.args.reports)
tt.want.err(t, err)
if tt.want.data != nil {
info, err := os.Stat(tt.args.outputDest)
require.NoError(t, err)
var filePath string
if info.IsDir() {
filePath = tt.args.outputDest + "/result.sarif"
} else { // if file was named with outputDest value
assert.Equal(t, tt.want.fileName, info.Name())
filePath = tt.args.outputDest
}
bytes, err := os.ReadFile(filePath)
require.NoError(t, err)
assert.Equal(t, tt.want.data, bytes)
err = os.Remove(filePath)
require.NoError(t, err)
}
},
)
}
}

func Test_JunitReporter_OutputBytesToFile(t *testing.T) {
report := Report{
"good.json",
Expand Down
136 changes: 136 additions & 0 deletions pkg/reporter/sarif_reporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package reporter

import (
"encoding/json"
"fmt"
"strings"
)

type SARIFReporter struct {
outputDest string
}

type SARIFLog struct {
Version string `json:"version"`
Schema string `json:"$schema"`
Runs []runs `json:"runs"`
}

type runs struct {
Tool tool `json:"tool"`
Artifacts []artifact `json:"artifacts"`
Results []result `json:"results"`
}

type tool struct {
Driver driver `json:"driver"`
}

type driver struct {
Name string `json:"name"`
InfoURI string `json:"informationUri"`
}

type artifact struct {
Location location `json:"location"`
}

type result struct {
Kind string `json:"kind"`
Level string `json:"level"`
Message message `json:"message"`
Locations []resultLocation `json:"locations"`
}

type message struct {
Text string `json:"text"`
}

type resultLocation struct {
PhysicalLocation physicalLocation `json:"physicalLocation"`
}

type physicalLocation struct {
Location location `json:"artifactLocation"`
}

type location struct {
URI string `json:"uri"`
Index *int `json:"index,omitempty"`
}

func NewSARIFReporter(outputDest string) *SARIFReporter {
return &SARIFReporter{
outputDest: outputDest,
}
}

func createSARIFReport(reports []Report) (SARIFLog, error) {
var log SARIFLog

n := len(reports)

log.Version = "2.1.0"
log.Schema = "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.4.json"

log.Runs = make([]runs, 1)
runs := &log.Runs[0]

runs.Tool.Driver.Name = "config-file-validator"
runs.Tool.Driver.InfoURI = "https://github.com/Boeing/config-file-validator"

runs.Artifacts = make([]artifact, n)
runs.Results = make([]result, n)

for i, report := range reports {
if strings.Contains(report.FilePath, "\\") {
report.FilePath = strings.ReplaceAll(report.FilePath, "\\", "/")
}

artifact := &runs.Artifacts[i]
artifact.Location.URI = report.FilePath

result := &runs.Results[i]
if !report.IsValid {
result.Kind = "fail"
result.Level = "error"
result.Message.Text = report.ValidationError.Error()
} else {
result.Kind = "pass"
result.Level = "none"
result.Message.Text = "No errors detected"
}

result.Locations = make([]resultLocation, 1)
location := &result.Locations[0]
location.PhysicalLocation.Location.URI = report.FilePath
location.PhysicalLocation.Location.Index = new(int)
*location.PhysicalLocation.Location.Index = i
}

return log, nil
}

func (sr SARIFReporter) Print(reports []Report) error {
report, err := createSARIFReport(reports)
if err != nil {
return err
}

sarifBytes, err := json.MarshalIndent(report, "", " ")
if err != nil {
return err
}

sarifBytes = append(sarifBytes, '\n')

if len(reports) > 0 && !reports[0].IsQuiet {
fmt.Print(string(sarifBytes))
}

if sr.outputDest != "" {
return outputBytesToFile(sr.outputDest, "result", "sarif", sarifBytes)
}

return nil
}
40 changes: 40 additions & 0 deletions test/output/example/result.sarif
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"version": "2.1.0",
"$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.4.json",
"runs": [
{
"tool": {
"driver": {
"name": "config-file-validator",
"informationUri": "https://github.com/Boeing/config-file-validator"
}
},
"artifacts": [
{
"location": {
"uri": "test/output/example/good.json"
}
}
],
"results": [
{
"kind": "pass",
"level": "none",
"message": {
"text": "No errors detected"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "test/output/example/good.json",
"index": 0
}
}
}
]
}
]
}
]
}
Loading