feature(formatter/text): Add color option on text format (#460)

* feature(issue): Add function to return file path and line number

* docs(formatter/CreateReport): Update formats accepted

* feature(formatter): Add color output for text format

Basic color support for text format. For now, only the "Summary" title
and "Issues" section has color

* feature(formatter): Highlight issues based on severity

Given an issue, the file path is painted based on its severity.
We're using the following rules: high is red, medium is yellow and
low is simple black & white

* feature(main): Add color flag

It's only valid for text format

* refactor(formatter): Passing color flag forward
This commit is contained in:
Marco Antônio Singer 2020-04-14 04:50:02 -03:00 committed by GitHub
parent 51e4317f09
commit 656691b387
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 122 additions and 22 deletions

View file

@ -120,6 +120,9 @@ var (
// exlude the folders from scan // exlude the folders from scan
flagDirsExclude arrayFlags flagDirsExclude arrayFlags
// set color on text format output
flagColor = flag.Bool("color", false, "Enable colored output. Valid for text format")
logger *log.Logger logger *log.Logger
) )
@ -187,7 +190,7 @@ func loadRules(include, exclude string) rules.RuleList {
return rules.Generate(filters...) return rules.Generate(filters...)
} }
func saveOutput(filename, format string, paths []string, issues []*gosec.Issue, metrics *gosec.Metrics, errors map[string][]gosec.Error) error { func saveOutput(filename, format string, color bool, paths []string, issues []*gosec.Issue, metrics *gosec.Metrics, errors map[string][]gosec.Error) error {
rootPaths := []string{} rootPaths := []string{}
for _, path := range paths { for _, path := range paths {
rootPath, err := gosec.RootPath(path) rootPath, err := gosec.RootPath(path)
@ -202,12 +205,12 @@ func saveOutput(filename, format string, paths []string, issues []*gosec.Issue,
return err return err
} }
defer outfile.Close() // #nosec G307 defer outfile.Close() // #nosec G307
err = output.CreateReport(outfile, format, rootPaths, issues, metrics, errors) err = output.CreateReport(outfile, format, color, rootPaths, issues, metrics, errors)
if err != nil { if err != nil {
return err return err
} }
} else { } else {
err := output.CreateReport(os.Stdout, format, rootPaths, issues, metrics, errors) err := output.CreateReport(os.Stdout, format, color, rootPaths, issues, metrics, errors)
if err != nil { if err != nil {
return err return err
} }
@ -282,6 +285,11 @@ func main() {
logger = log.New(logWriter, "[gosec] ", log.LstdFlags) logger = log.New(logWriter, "[gosec] ", log.LstdFlags)
} }
// Color flag is allowed for text format
if *flagColor && *flagFormat != "text" {
logger.Fatalf("cannot set color with %s format. Only text format is accepted", *flagFormat)
}
failSeverity, err := convertToScore(*flagSeverity) failSeverity, err := convertToScore(*flagSeverity)
if err != nil { if err != nil {
logger.Fatalf("Invalid severity value: %v", err) logger.Fatalf("Invalid severity value: %v", err)
@ -350,7 +358,7 @@ func main() {
} }
// Create output report // Create output report
if err := saveOutput(*flagOutput, *flagFormat, flag.Args(), issues, metrics, errors); err != nil { if err := saveOutput(*flagOutput, *flagFormat, *flagColor, flag.Args(), issues, metrics, errors); err != nil {
logger.Fatal(err) logger.Fatal(err)
} }

1
go.mod
View file

@ -13,6 +13,7 @@ require (
golang.org/x/text v0.3.2 // indirect golang.org/x/text v0.3.2 // indirect
golang.org/x/tools v0.0.0-20200331202046-9d5940d49312 golang.org/x/tools v0.0.0-20200331202046-9d5940d49312
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/gookit/color.v1 v1.1.6
gopkg.in/yaml.v2 v2.2.8 gopkg.in/yaml.v2 v2.2.8
) )

2
go.sum
View file

@ -71,6 +71,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gookit/color.v1 v1.1.6 h1:5fB10p6AUFjhd2ayq9JgmJWr9WlTrguFdw3qlYtKNHk=
gopkg.in/gookit/color.v1 v1.1.6/go.mod h1:IcEkFGaveVShJ+j8ew+jwe9epHyGpJ9IrptHmW3laVY=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=

View file

@ -88,6 +88,11 @@ type Issue struct {
Col string `json:"column"` // Column number in line Col string `json:"column"` // Column number in line
} }
// FileLocation point out the file path and line number in file
func (i Issue) FileLocation() string {
return fmt.Sprintf("%s:%s", i.File, i.Line)
}
// MetaData is embedded in all gosec rules. The Severity, Confidence and What message // MetaData is embedded in all gosec rules. The Severity, Confidence and What message
// will be passed through to reported issues. // will be passed through to reported issues.
type MetaData struct { type MetaData struct {

View file

@ -49,6 +49,43 @@ var _ = Describe("Issue", func() {
Skip("Not implemented") Skip("Not implemented")
}) })
It("should construct file path based on line and file information", func() {
var target *ast.AssignStmt
source := `package main
import "fmt"
func main() {
username := "admin"
password := "f62e5bcda4fae4f82370da0c6f20697b8f8447ef"
fmt.Println("Doing something with: ", username, password)
}`
pkg := testutils.NewTestPackage()
defer pkg.Close()
pkg.AddFile("foo.go", source)
ctx := pkg.CreateContext("foo.go")
v := testutils.NewMockVisitor()
v.Callback = func(n ast.Node, ctx *gosec.Context) bool {
if node, ok := n.(*ast.AssignStmt); ok {
if id, ok := node.Lhs[0].(*ast.Ident); ok && id.Name == "password" {
target = node
}
}
return true
}
v.Context = ctx
ast.Walk(v, ctx.Root)
Expect(target).ShouldNot(BeNil())
// Use hardcodeded rule to check assignment
cfg := gosec.NewConfig()
rule, _ := rules.NewHardcodedCredentials("TEST", cfg)
issue, err := rule.Match(target, ctx)
Expect(err).ShouldNot(HaveOccurred())
Expect(issue).ShouldNot(BeNil())
Expect(issue.FileLocation()).Should(MatchRegexp("foo.go:5"))
})
It("should provide accurate line and file information", func() { It("should provide accurate line and file information", func() {
Skip("Not implemented") Skip("Not implemented")
}) })

View file

@ -26,6 +26,7 @@ import (
plainTemplate "text/template" plainTemplate "text/template"
"github.com/securego/gosec/v2" "github.com/securego/gosec/v2"
color "gopkg.in/gookit/color.v1"
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
@ -57,15 +58,19 @@ Golang errors in file: [{{ $filePath }}]:
{{end}} {{end}}
{{end}} {{end}}
{{ range $index, $issue := .Issues }} {{ range $index, $issue := .Issues }}
[{{ $issue.File }}:{{ $issue.Line }}] - {{ $issue.RuleID }} (CWE-{{ $issue.Cwe.ID }}): {{ $issue.What }} (Confidence: {{ $issue.Confidence}}, Severity: {{ $issue.Severity }}) [{{ highlight $issue.FileLocation $issue.Severity }}] - {{ $issue.RuleID }} (CWE-{{ $issue.Cwe.ID }}): {{ $issue.What }} (Confidence: {{ $issue.Confidence}}, Severity: {{ $issue.Severity }})
> {{ $issue.Code }} > {{ $issue.Code }}
{{ end }} {{ end }}
Summary: {{ notice "Summary:" }}
Files: {{.Stats.NumFiles}} Files: {{.Stats.NumFiles}}
Lines: {{.Stats.NumLines}} Lines: {{.Stats.NumLines}}
Nosec: {{.Stats.NumNosec}} Nosec: {{.Stats.NumNosec}}
Issues: {{.Stats.NumFound}} Issues: {{ if eq .Stats.NumFound 0 }}
{{- success .Stats.NumFound }}
{{- else }}
{{- danger .Stats.NumFound }}
{{- end }}
` `
@ -76,8 +81,8 @@ type reportInfo struct {
} }
// CreateReport generates a report based for the supplied issues and metrics given // CreateReport generates a report based for the supplied issues and metrics given
// the specified format. The formats currently accepted are: json, csv, html and text. // the specified format. The formats currently accepted are: json, yaml, csv, junit-xml, html, sonarqube, golint and text.
func CreateReport(w io.Writer, format string, rootPaths []string, issues []*gosec.Issue, metrics *gosec.Metrics, errors map[string][]gosec.Error) error { func CreateReport(w io.Writer, format string, enableColor bool, rootPaths []string, issues []*gosec.Issue, metrics *gosec.Metrics, errors map[string][]gosec.Error) error {
data := &reportInfo{ data := &reportInfo{
Errors: errors, Errors: errors,
Issues: issues, Issues: issues,
@ -96,13 +101,13 @@ func CreateReport(w io.Writer, format string, rootPaths []string, issues []*gose
case "html": case "html":
err = reportFromHTMLTemplate(w, html, data) err = reportFromHTMLTemplate(w, html, data)
case "text": case "text":
err = reportFromPlaintextTemplate(w, text, data) err = reportFromPlaintextTemplate(w, text, enableColor, data)
case "sonarqube": case "sonarqube":
err = reportSonarqube(rootPaths, w, data) err = reportSonarqube(rootPaths, w, data)
case "golint": case "golint":
err = reportGolint(w, data) err = reportGolint(w, data)
default: default:
err = reportFromPlaintextTemplate(w, text, data) err = reportFromPlaintextTemplate(w, text, enableColor, data)
} }
return err return err
} }
@ -253,8 +258,11 @@ func reportJUnitXML(w io.Writer, data *reportInfo) error {
return nil return nil
} }
func reportFromPlaintextTemplate(w io.Writer, reportTemplate string, data *reportInfo) error { func reportFromPlaintextTemplate(w io.Writer, reportTemplate string, enableColor bool, data *reportInfo) error {
t, e := plainTemplate.New("gosec").Parse(reportTemplate) t, e := plainTemplate.
New("gosec").
Funcs(plainTextFuncMap(enableColor)).
Parse(reportTemplate)
if e != nil { if e != nil {
return e return e
} }
@ -270,3 +278,42 @@ func reportFromHTMLTemplate(w io.Writer, reportTemplate string, data *reportInfo
return t.Execute(w, data) return t.Execute(w, data)
} }
func plainTextFuncMap(enableColor bool) plainTemplate.FuncMap {
if enableColor {
return plainTemplate.FuncMap{
"highlight": highlight,
"danger": color.Danger.Render,
"notice": color.Notice.Render,
"success": color.Success.Render,
}
}
// by default those functions return the given content untouched
return plainTemplate.FuncMap{
"highlight": func(t string, s gosec.Score) string {
return t
},
"danger": fmt.Sprint,
"notice": fmt.Sprint,
"success": fmt.Sprint,
}
}
var (
errorTheme = color.New(color.FgLightWhite, color.BgRed)
warningTheme = color.New(color.FgBlack, color.BgYellow)
defaultTheme = color.New(color.FgWhite, color.BgBlack)
)
// highlight returns content t colored based on Score
func highlight(t string, s gosec.Score) string {
switch s {
case gosec.High:
return errorTheme.Sprint(t)
case gosec.Medium:
return warningTheme.Sprint(t)
default:
return defaultTheme.Sprint(t)
}
}

View file

@ -262,7 +262,7 @@ var _ = Describe("Formatter", func() {
error := map[string][]gosec.Error{} error := map[string][]gosec.Error{}
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
err := CreateReport(buf, "csv", []string{}, []*gosec.Issue{&issue}, &gosec.Metrics{}, error) err := CreateReport(buf, "csv", false, []string{}, []*gosec.Issue{&issue}, &gosec.Metrics{}, error)
Expect(err).ShouldNot(HaveOccurred()) Expect(err).ShouldNot(HaveOccurred())
pattern := "/home/src/project/test.go,1,test,HIGH,HIGH,testcode,CWE-%s\n" pattern := "/home/src/project/test.go,1,test,HIGH,HIGH,testcode,CWE-%s\n"
expect := fmt.Sprintf(pattern, cwe.ID) expect := fmt.Sprintf(pattern, cwe.ID)
@ -276,7 +276,7 @@ var _ = Describe("Formatter", func() {
error := map[string][]gosec.Error{} error := map[string][]gosec.Error{}
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
err := CreateReport(buf, "xml", []string{}, []*gosec.Issue{&issue}, &gosec.Metrics{NumFiles: 0, NumLines: 0, NumNosec: 0, NumFound: 0}, error) err := CreateReport(buf, "xml", false, []string{}, []*gosec.Issue{&issue}, &gosec.Metrics{NumFiles: 0, NumLines: 0, NumNosec: 0, NumFound: 0}, error)
Expect(err).ShouldNot(HaveOccurred()) Expect(err).ShouldNot(HaveOccurred())
pattern := "Results:\n\n\n[/home/src/project/test.go:1] - %s (CWE-%s): test (Confidence: HIGH, Severity: HIGH)\n > testcode\n\n\nSummary:\n Files: 0\n Lines: 0\n Nosec: 0\n Issues: 0\n\n" pattern := "Results:\n\n\n[/home/src/project/test.go:1] - %s (CWE-%s): test (Confidence: HIGH, Severity: HIGH)\n > testcode\n\n\nSummary:\n Files: 0\n Lines: 0\n Nosec: 0\n Issues: 0\n\n"
expect := fmt.Sprintf(pattern, rule, cwe.ID) expect := fmt.Sprintf(pattern, rule, cwe.ID)
@ -296,7 +296,7 @@ var _ = Describe("Formatter", func() {
err := enc.Encode(data) err := enc.Encode(data)
Expect(err).ShouldNot(HaveOccurred()) Expect(err).ShouldNot(HaveOccurred())
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
err = CreateReport(buf, "json", []string{}, []*gosec.Issue{&issue}, &gosec.Metrics{}, error) err = CreateReport(buf, "json", false, []string{}, []*gosec.Issue{&issue}, &gosec.Metrics{}, error)
Expect(err).ShouldNot(HaveOccurred()) Expect(err).ShouldNot(HaveOccurred())
result := stripString(buf.String()) result := stripString(buf.String())
expectation := stripString(expect.String()) expectation := stripString(expect.String())
@ -316,7 +316,7 @@ var _ = Describe("Formatter", func() {
err := enc.Encode(data) err := enc.Encode(data)
Expect(err).ShouldNot(HaveOccurred()) Expect(err).ShouldNot(HaveOccurred())
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
err = CreateReport(buf, "html", []string{}, []*gosec.Issue{&issue}, &gosec.Metrics{}, error) err = CreateReport(buf, "html", false, []string{}, []*gosec.Issue{&issue}, &gosec.Metrics{}, error)
Expect(err).ShouldNot(HaveOccurred()) Expect(err).ShouldNot(HaveOccurred())
result := stripString(buf.String()) result := stripString(buf.String())
expectation := stripString(expect.String()) expectation := stripString(expect.String())
@ -336,7 +336,7 @@ var _ = Describe("Formatter", func() {
err := enc.Encode(data) err := enc.Encode(data)
Expect(err).ShouldNot(HaveOccurred()) Expect(err).ShouldNot(HaveOccurred())
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
err = CreateReport(buf, "yaml", []string{}, []*gosec.Issue{&issue}, &gosec.Metrics{}, error) err = CreateReport(buf, "yaml", false, []string{}, []*gosec.Issue{&issue}, &gosec.Metrics{}, error)
Expect(err).ShouldNot(HaveOccurred()) Expect(err).ShouldNot(HaveOccurred())
result := stripString(buf.String()) result := stripString(buf.String())
expectation := stripString(expect.String()) expectation := stripString(expect.String())
@ -356,7 +356,7 @@ var _ = Describe("Formatter", func() {
err := enc.Encode(data) err := enc.Encode(data)
Expect(err).ShouldNot(HaveOccurred()) Expect(err).ShouldNot(HaveOccurred())
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
err = CreateReport(buf, "junit-xml", []string{}, []*gosec.Issue{&issue}, &gosec.Metrics{}, error) err = CreateReport(buf, "junit-xml", false, []string{}, []*gosec.Issue{&issue}, &gosec.Metrics{}, error)
Expect(err).ShouldNot(HaveOccurred()) Expect(err).ShouldNot(HaveOccurred())
expectation := stripString(fmt.Sprintf("[/home/src/project/test.go:1] - test (Confidence: 2, Severity: 2, CWE: %s)", cwe.ID)) expectation := stripString(fmt.Sprintf("[/home/src/project/test.go:1] - test (Confidence: 2, Severity: 2, CWE: %s)", cwe.ID))
result := stripString(buf.String()) result := stripString(buf.String())
@ -376,7 +376,7 @@ var _ = Describe("Formatter", func() {
err := enc.Encode(data) err := enc.Encode(data)
Expect(err).ShouldNot(HaveOccurred()) Expect(err).ShouldNot(HaveOccurred())
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
err = CreateReport(buf, "text", []string{}, []*gosec.Issue{&issue}, &gosec.Metrics{}, error) err = CreateReport(buf, "text", false, []string{}, []*gosec.Issue{&issue}, &gosec.Metrics{}, error)
Expect(err).ShouldNot(HaveOccurred()) Expect(err).ShouldNot(HaveOccurred())
expectation := stripString(fmt.Sprintf("[/home/src/project/test.go:1] - %s (CWE-%s): test (Confidence: HIGH, Severity: HIGH)", rule, cwe.ID)) expectation := stripString(fmt.Sprintf("[/home/src/project/test.go:1] - %s (CWE-%s): test (Confidence: HIGH, Severity: HIGH)", rule, cwe.ID))
result := stripString(buf.String()) result := stripString(buf.String())
@ -389,7 +389,7 @@ var _ = Describe("Formatter", func() {
issue := createIssue(rule, cwe) issue := createIssue(rule, cwe)
error := map[string][]gosec.Error{} error := map[string][]gosec.Error{}
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
err := CreateReport(buf, "sonarqube", []string{"/home/src/project"}, []*gosec.Issue{&issue}, &gosec.Metrics{}, error) err := CreateReport(buf, "sonarqube", false, []string{"/home/src/project"}, []*gosec.Issue{&issue}, &gosec.Metrics{}, error)
Expect(err).ShouldNot(HaveOccurred()) Expect(err).ShouldNot(HaveOccurred())
result := stripString(buf.String()) result := stripString(buf.String())
@ -410,7 +410,7 @@ var _ = Describe("Formatter", func() {
error := map[string][]gosec.Error{} error := map[string][]gosec.Error{}
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
err := CreateReport(buf, "golint", []string{}, []*gosec.Issue{&issue}, &gosec.Metrics{}, error) err := CreateReport(buf, "golint", false, []string{}, []*gosec.Issue{&issue}, &gosec.Metrics{}, error)
Expect(err).ShouldNot(HaveOccurred()) Expect(err).ShouldNot(HaveOccurred())
pattern := "/home/src/project/test.go:1:1: [CWE-%s] test (Rule:%s, Severity:HIGH, Confidence:HIGH)\n" pattern := "/home/src/project/test.go:1:1: [CWE-%s] test (Rule:%s, Severity:HIGH, Confidence:HIGH)\n"
expect := fmt.Sprintf(pattern, cwe.ID, rule) expect := fmt.Sprintf(pattern, cwe.ID, rule)