mirror of
https://github.com/securego/gosec.git
synced 2024-12-24 11:35:52 +00:00
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:
parent
51e4317f09
commit
656691b387
7 changed files with 122 additions and 22 deletions
|
@ -120,6 +120,9 @@ var (
|
|||
// exlude the folders from scan
|
||||
flagDirsExclude arrayFlags
|
||||
|
||||
// set color on text format output
|
||||
flagColor = flag.Bool("color", false, "Enable colored output. Valid for text format")
|
||||
|
||||
logger *log.Logger
|
||||
)
|
||||
|
||||
|
@ -187,7 +190,7 @@ func loadRules(include, exclude string) rules.RuleList {
|
|||
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{}
|
||||
for _, path := range paths {
|
||||
rootPath, err := gosec.RootPath(path)
|
||||
|
@ -202,12 +205,12 @@ func saveOutput(filename, format string, paths []string, issues []*gosec.Issue,
|
|||
return err
|
||||
}
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
} 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 {
|
||||
return err
|
||||
}
|
||||
|
@ -282,6 +285,11 @@ func main() {
|
|||
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)
|
||||
if err != nil {
|
||||
logger.Fatalf("Invalid severity value: %v", err)
|
||||
|
@ -350,7 +358,7 @@ func main() {
|
|||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
|
|
1
go.mod
1
go.mod
|
@ -13,6 +13,7 @@ require (
|
|||
golang.org/x/text v0.3.2 // indirect
|
||||
golang.org/x/tools v0.0.0-20200331202046-9d5940d49312
|
||||
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
|
||||
)
|
||||
|
||||
|
|
2
go.sum
2
go.sum
|
@ -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/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
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/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
|
|
5
issue.go
5
issue.go
|
@ -88,6 +88,11 @@ type Issue struct {
|
|||
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
|
||||
// will be passed through to reported issues.
|
||||
type MetaData struct {
|
||||
|
|
|
@ -49,6 +49,43 @@ var _ = Describe("Issue", func() {
|
|||
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() {
|
||||
Skip("Not implemented")
|
||||
})
|
||||
|
|
|
@ -26,6 +26,7 @@ import (
|
|||
plainTemplate "text/template"
|
||||
|
||||
"github.com/securego/gosec/v2"
|
||||
color "gopkg.in/gookit/color.v1"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
|
@ -57,15 +58,19 @@ Golang errors in file: [{{ $filePath }}]:
|
|||
{{end}}
|
||||
{{end}}
|
||||
{{ 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 }}
|
||||
|
||||
{{ end }}
|
||||
Summary:
|
||||
{{ notice "Summary:" }}
|
||||
Files: {{.Stats.NumFiles}}
|
||||
Lines: {{.Stats.NumLines}}
|
||||
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
|
||||
// the specified format. The formats currently accepted are: json, csv, html and text.
|
||||
func CreateReport(w io.Writer, format string, rootPaths []string, issues []*gosec.Issue, metrics *gosec.Metrics, errors map[string][]gosec.Error) error {
|
||||
// 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, enableColor bool, rootPaths []string, issues []*gosec.Issue, metrics *gosec.Metrics, errors map[string][]gosec.Error) error {
|
||||
data := &reportInfo{
|
||||
Errors: errors,
|
||||
Issues: issues,
|
||||
|
@ -96,13 +101,13 @@ func CreateReport(w io.Writer, format string, rootPaths []string, issues []*gose
|
|||
case "html":
|
||||
err = reportFromHTMLTemplate(w, html, data)
|
||||
case "text":
|
||||
err = reportFromPlaintextTemplate(w, text, data)
|
||||
err = reportFromPlaintextTemplate(w, text, enableColor, data)
|
||||
case "sonarqube":
|
||||
err = reportSonarqube(rootPaths, w, data)
|
||||
case "golint":
|
||||
err = reportGolint(w, data)
|
||||
default:
|
||||
err = reportFromPlaintextTemplate(w, text, data)
|
||||
err = reportFromPlaintextTemplate(w, text, enableColor, data)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
@ -253,8 +258,11 @@ func reportJUnitXML(w io.Writer, data *reportInfo) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func reportFromPlaintextTemplate(w io.Writer, reportTemplate string, data *reportInfo) error {
|
||||
t, e := plainTemplate.New("gosec").Parse(reportTemplate)
|
||||
func reportFromPlaintextTemplate(w io.Writer, reportTemplate string, enableColor bool, data *reportInfo) error {
|
||||
t, e := plainTemplate.
|
||||
New("gosec").
|
||||
Funcs(plainTextFuncMap(enableColor)).
|
||||
Parse(reportTemplate)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
@ -270,3 +278,42 @@ func reportFromHTMLTemplate(w io.Writer, reportTemplate string, data *reportInfo
|
|||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -262,7 +262,7 @@ var _ = Describe("Formatter", func() {
|
|||
error := map[string][]gosec.Error{}
|
||||
|
||||
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())
|
||||
pattern := "/home/src/project/test.go,1,test,HIGH,HIGH,testcode,CWE-%s\n"
|
||||
expect := fmt.Sprintf(pattern, cwe.ID)
|
||||
|
@ -276,7 +276,7 @@ var _ = Describe("Formatter", func() {
|
|||
error := map[string][]gosec.Error{}
|
||||
|
||||
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())
|
||||
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)
|
||||
|
@ -296,7 +296,7 @@ var _ = Describe("Formatter", func() {
|
|||
err := enc.Encode(data)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
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())
|
||||
result := stripString(buf.String())
|
||||
expectation := stripString(expect.String())
|
||||
|
@ -316,7 +316,7 @@ var _ = Describe("Formatter", func() {
|
|||
err := enc.Encode(data)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
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())
|
||||
result := stripString(buf.String())
|
||||
expectation := stripString(expect.String())
|
||||
|
@ -336,7 +336,7 @@ var _ = Describe("Formatter", func() {
|
|||
err := enc.Encode(data)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
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())
|
||||
result := stripString(buf.String())
|
||||
expectation := stripString(expect.String())
|
||||
|
@ -356,7 +356,7 @@ var _ = Describe("Formatter", func() {
|
|||
err := enc.Encode(data)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
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())
|
||||
expectation := stripString(fmt.Sprintf("[/home/src/project/test.go:1] - test (Confidence: 2, Severity: 2, CWE: %s)", cwe.ID))
|
||||
result := stripString(buf.String())
|
||||
|
@ -376,7 +376,7 @@ var _ = Describe("Formatter", func() {
|
|||
err := enc.Encode(data)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
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())
|
||||
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())
|
||||
|
@ -389,7 +389,7 @@ var _ = Describe("Formatter", func() {
|
|||
issue := createIssue(rule, cwe)
|
||||
error := map[string][]gosec.Error{}
|
||||
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())
|
||||
|
||||
result := stripString(buf.String())
|
||||
|
@ -410,7 +410,7 @@ var _ = Describe("Formatter", func() {
|
|||
error := map[string][]gosec.Error{}
|
||||
|
||||
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())
|
||||
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)
|
||||
|
|
Loading…
Reference in a new issue