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