1// (c) Copyright 2016 Hewlett Packard Enterprise Development LP
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package output
16
17import (
18	"bufio"
19	"bytes"
20	"encoding/csv"
21	"encoding/json"
22	"encoding/xml"
23	"fmt"
24	htmlTemplate "html/template"
25	"io"
26	"strconv"
27	"strings"
28	plainTemplate "text/template"
29
30	color "github.com/gookit/color"
31	"github.com/securego/gosec/v2"
32	"gopkg.in/yaml.v2"
33)
34
35// ReportFormat enumerates the output format for reported issues
36type ReportFormat int
37
38const (
39	// ReportText is the default format that writes to stdout
40	ReportText ReportFormat = iota // Plain text format
41
42	// ReportJSON set the output format to json
43	ReportJSON // Json format
44
45	// ReportCSV set the output format to csv
46	ReportCSV // CSV format
47
48	// ReportJUnitXML set the output format to junit xml
49	ReportJUnitXML // JUnit XML format
50
51	// ReportSARIF set the output format to SARIF
52	ReportSARIF // SARIF format
53
54	//SonarqubeEffortMinutes effort to fix in minutes
55	SonarqubeEffortMinutes = 5
56)
57
58var text = `Results:
59{{range $filePath,$fileErrors := .Errors}}
60Golang errors in file: [{{ $filePath }}]:
61{{range $index, $error := $fileErrors}}
62  > [line {{$error.Line}} : column {{$error.Column}}] - {{$error.Err}}
63{{end}}
64{{end}}
65{{ range $index, $issue := .Issues }}
66[{{ highlight $issue.FileLocation $issue.Severity }}] - {{ $issue.RuleID }} (CWE-{{ $issue.Cwe.ID }}): {{ $issue.What }} (Confidence: {{ $issue.Confidence}}, Severity: {{ $issue.Severity }})
67{{ printCode $issue }}
68
69{{ end }}
70{{ notice "Summary:" }}
71   Files: {{.Stats.NumFiles}}
72   Lines: {{.Stats.NumLines}}
73   Nosec: {{.Stats.NumNosec}}
74  Issues: {{ if eq .Stats.NumFound 0 }}
75	{{- success .Stats.NumFound }}
76	{{- else }}
77	{{- danger .Stats.NumFound }}
78	{{- end }}
79
80`
81
82type reportInfo struct {
83	Errors map[string][]gosec.Error `json:"Golang errors"`
84	Issues []*gosec.Issue
85	Stats  *gosec.Metrics
86}
87
88// CreateReport generates a report based for the supplied issues and metrics given
89// the specified format. The formats currently accepted are: json, yaml, csv, junit-xml, html, sonarqube, golint and text.
90func CreateReport(w io.Writer, format string, enableColor bool, rootPaths []string, issues []*gosec.Issue, metrics *gosec.Metrics, errors map[string][]gosec.Error) error {
91	data := &reportInfo{
92		Errors: errors,
93		Issues: issues,
94		Stats:  metrics,
95	}
96	var err error
97	switch format {
98	case "json":
99		err = reportJSON(w, data)
100	case "yaml":
101		err = reportYAML(w, data)
102	case "csv":
103		err = reportCSV(w, data)
104	case "junit-xml":
105		err = reportJUnitXML(w, data)
106	case "html":
107		err = reportFromHTMLTemplate(w, html, data)
108	case "text":
109		err = reportFromPlaintextTemplate(w, text, enableColor, data)
110	case "sonarqube":
111		err = reportSonarqube(rootPaths, w, data)
112	case "golint":
113		err = reportGolint(w, data)
114	case "sarif":
115		err = reportSARIFTemplate(rootPaths, w, data)
116	default:
117		err = reportFromPlaintextTemplate(w, text, enableColor, data)
118	}
119	return err
120}
121
122func reportSonarqube(rootPaths []string, w io.Writer, data *reportInfo) error {
123	si, err := convertToSonarIssues(rootPaths, data)
124	if err != nil {
125		return err
126	}
127	raw, err := json.MarshalIndent(si, "", "\t")
128	if err != nil {
129		return err
130	}
131	_, err = w.Write(raw)
132	return err
133}
134
135func convertToSonarIssues(rootPaths []string, data *reportInfo) (*sonarIssues, error) {
136	si := &sonarIssues{[]sonarIssue{}}
137	for _, issue := range data.Issues {
138		var sonarFilePath string
139		for _, rootPath := range rootPaths {
140			if strings.HasPrefix(issue.File, rootPath) {
141				sonarFilePath = strings.Replace(issue.File, rootPath+"/", "", 1)
142			}
143		}
144
145		if sonarFilePath == "" {
146			continue
147		}
148
149		lines := strings.Split(issue.Line, "-")
150		startLine, err := strconv.Atoi(lines[0])
151		if err != nil {
152			return si, err
153		}
154		endLine := startLine
155		if len(lines) > 1 {
156			endLine, err = strconv.Atoi(lines[1])
157			if err != nil {
158				return si, err
159			}
160		}
161
162		s := sonarIssue{
163			EngineID: "gosec",
164			RuleID:   issue.RuleID,
165			PrimaryLocation: location{
166				Message:   issue.What,
167				FilePath:  sonarFilePath,
168				TextRange: textRange{StartLine: startLine, EndLine: endLine},
169			},
170			Type:          "VULNERABILITY",
171			Severity:      getSonarSeverity(issue.Severity.String()),
172			EffortMinutes: SonarqubeEffortMinutes,
173			Cwe:           issue.Cwe,
174		}
175		si.SonarIssues = append(si.SonarIssues, s)
176	}
177	return si, nil
178}
179
180func convertToSarifReport(rootPaths []string, data *reportInfo) (*sarifReport, error) {
181	sr := buildSarifReport()
182
183	var rules []*sarifRule
184	var locations []*sarifLocation
185	results := []*sarifResult{}
186
187	for index, issue := range data.Issues {
188		rules = append(rules, buildSarifRule(issue))
189
190		location, err := buildSarifLocation(issue, rootPaths)
191		if err != nil {
192			return nil, err
193		}
194		locations = append(locations, location)
195
196		result := &sarifResult{
197			RuleID:    fmt.Sprintf("%s (CWE-%s)", issue.RuleID, issue.Cwe.ID),
198			RuleIndex: index,
199			Level:     getSarifLevel(issue.Severity.String()),
200			Message: &sarifMessage{
201				Text: issue.What,
202			},
203			Locations: locations,
204		}
205
206		results = append(results, result)
207	}
208
209	tool := &sarifTool{
210		Driver: &sarifDriver{
211			Name:           "gosec",
212			InformationURI: "https://github.com/securego/gosec/",
213			Rules:          rules,
214		},
215	}
216
217	run := &sarifRun{
218		Tool:    tool,
219		Results: results,
220	}
221
222	sr.Runs = append(sr.Runs, run)
223
224	return sr, nil
225}
226
227func reportJSON(w io.Writer, data *reportInfo) error {
228	raw, err := json.MarshalIndent(data, "", "\t")
229	if err != nil {
230		return err
231	}
232
233	_, err = w.Write(raw)
234	return err
235}
236
237func reportYAML(w io.Writer, data *reportInfo) error {
238	raw, err := yaml.Marshal(data)
239	if err != nil {
240		return err
241	}
242	_, err = w.Write(raw)
243	return err
244}
245
246func reportCSV(w io.Writer, data *reportInfo) error {
247	out := csv.NewWriter(w)
248	defer out.Flush()
249	for _, issue := range data.Issues {
250		err := out.Write([]string{
251			issue.File,
252			issue.Line,
253			issue.What,
254			issue.Severity.String(),
255			issue.Confidence.String(),
256			issue.Code,
257			fmt.Sprintf("CWE-%s", issue.Cwe.ID),
258		})
259		if err != nil {
260			return err
261		}
262	}
263	return nil
264}
265
266func reportGolint(w io.Writer, data *reportInfo) error {
267	// Output Sample:
268	// /tmp/main.go:11:14: [CWE-310] RSA keys should be at least 2048 bits (Rule:G403, Severity:MEDIUM, Confidence:HIGH)
269
270	for _, issue := range data.Issues {
271		what := issue.What
272		if issue.Cwe.ID != "" {
273			what = fmt.Sprintf("[CWE-%s] %s", issue.Cwe.ID, issue.What)
274		}
275
276		// issue.Line uses "start-end" format for multiple line detection.
277		lines := strings.Split(issue.Line, "-")
278		start := lines[0]
279
280		_, err := fmt.Fprintf(w, "%s:%s:%s: %s (Rule:%s, Severity:%s, Confidence:%s)\n",
281			issue.File,
282			start,
283			issue.Col,
284			what,
285			issue.RuleID,
286			issue.Severity.String(),
287			issue.Confidence.String(),
288		)
289		if err != nil {
290			return err
291		}
292	}
293	return nil
294}
295
296func reportJUnitXML(w io.Writer, data *reportInfo) error {
297	junitXMLStruct := createJUnitXMLStruct(data)
298	raw, err := xml.MarshalIndent(junitXMLStruct, "", "\t")
299	if err != nil {
300		return err
301	}
302
303	xmlHeader := []byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
304	raw = append(xmlHeader, raw...)
305	_, err = w.Write(raw)
306	if err != nil {
307		return err
308	}
309
310	return nil
311}
312
313func reportSARIFTemplate(rootPaths []string, w io.Writer, data *reportInfo) error {
314	sr, err := convertToSarifReport(rootPaths, data)
315	if err != nil {
316		return err
317	}
318	raw, err := json.MarshalIndent(sr, "", "\t")
319	if err != nil {
320		return err
321	}
322
323	_, err = w.Write(raw)
324	return err
325}
326
327func reportFromPlaintextTemplate(w io.Writer, reportTemplate string, enableColor bool, data *reportInfo) error {
328	t, e := plainTemplate.
329		New("gosec").
330		Funcs(plainTextFuncMap(enableColor)).
331		Parse(reportTemplate)
332	if e != nil {
333		return e
334	}
335
336	return t.Execute(w, data)
337}
338
339func reportFromHTMLTemplate(w io.Writer, reportTemplate string, data *reportInfo) error {
340	t, e := htmlTemplate.New("gosec").Parse(reportTemplate)
341	if e != nil {
342		return e
343	}
344
345	return t.Execute(w, data)
346}
347
348func plainTextFuncMap(enableColor bool) plainTemplate.FuncMap {
349	if enableColor {
350		return plainTemplate.FuncMap{
351			"highlight": highlight,
352			"danger":    color.Danger.Render,
353			"notice":    color.Notice.Render,
354			"success":   color.Success.Render,
355			"printCode": printCodeSnippet,
356		}
357	}
358
359	// by default those functions return the given content untouched
360	return plainTemplate.FuncMap{
361		"highlight": func(t string, s gosec.Score) string {
362			return t
363		},
364		"danger":    fmt.Sprint,
365		"notice":    fmt.Sprint,
366		"success":   fmt.Sprint,
367		"printCode": printCodeSnippet,
368	}
369}
370
371var (
372	errorTheme   = color.New(color.FgLightWhite, color.BgRed)
373	warningTheme = color.New(color.FgBlack, color.BgYellow)
374	defaultTheme = color.New(color.FgWhite, color.BgBlack)
375)
376
377// highlight returns content t colored based on Score
378func highlight(t string, s gosec.Score) string {
379	switch s {
380	case gosec.High:
381		return errorTheme.Sprint(t)
382	case gosec.Medium:
383		return warningTheme.Sprint(t)
384	default:
385		return defaultTheme.Sprint(t)
386	}
387}
388
389// printCodeSnippet prints the code snippet from the issue by adding a marker to the affected line
390func printCodeSnippet(issue *gosec.Issue) string {
391	start, end := parseLine(issue.Line)
392	scanner := bufio.NewScanner(strings.NewReader(issue.Code))
393	var buf bytes.Buffer
394	line := start
395	for scanner.Scan() {
396		codeLine := scanner.Text()
397		if strings.HasPrefix(codeLine, strconv.Itoa(line)) && line <= end {
398			codeLine = "  > " + codeLine + "\n"
399			line++
400		} else {
401			codeLine = "    " + codeLine + "\n"
402		}
403		buf.WriteString(codeLine)
404	}
405	return buf.String()
406}
407
408// parseLine extract the start and the end line numbers from a issue line
409func parseLine(line string) (int, int) {
410	parts := strings.Split(line, "-")
411	start := parts[0]
412	end := start
413	if len(parts) > 1 {
414		end = parts[1]
415	}
416	s, err := strconv.Atoi(start)
417	if err != nil {
418		return -1, -1
419	}
420	e, err := strconv.Atoi(end)
421	if err != nil {
422		return -1, -1
423	}
424	return s, e
425}
426