1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-03-05 15:15:44 +02:00
Oliver Nocon 1e2f2d93e3
feat: add commit and pipeline to GitHub reporting (#3930)
Co-authored-by: Sven Merk <33895725+nevskrem@users.noreply.github.com>
2022-08-15 07:48:22 +02:00

452 lines
11 KiB
Go

package reporting
import (
"bytes"
"encoding/json"
"fmt"
"text/template"
"time"
"github.com/SAP/jenkins-library/pkg/orchestrator"
"github.com/pkg/errors"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
// IssueDetail represents any content that can be transformed into the body of a GitHub issue
type IssueDetail interface {
Title() string
ToMarkdown() ([]byte, error)
ToTxt() string
}
// VulnerabilityReport represents metadata for a report on a vulnerability
type VulnerabilityReport struct {
ArtifactID string
Branch string
CommitID string
Description string
DirectDependency string
Footer string
Group string
PackageURL string
PipelineName string
PipelineLink string
PublishDate string
Resolution string
Score float64
Severity string
Version string
VulnerabilityLink string
VulnerabilityName string
}
const vulnerabilityMdTemplate string = `# {{title .Severity }} ({{ .Score }}) Vulnerability {{ .VulnerabilityName }} - {{ .ArtifactID }}
**Vulnerability link:** [{{ .VulnerabilityLink }}]({{ .VulnerabilityLink }})
## Fix
**{{ .Resolution }}**
## Context
{{if .PipelineLink -}}
### Pipeline
Pipeline run: [{{ .PipelineName }}]({{ .PipelineLink }})
{{- end}}
### Detected in
{{if .Branch}}**Branch:** {{ .Branch }}{{- end}}
{{if .CommitID}}**CommitId:** {{ .CommitID }}{{- end}}
{{if .DirectDependency}}**Dependency:** {{if (eq .DirectDependency "true")}}direct{{ else }}indirect{{ end }}{{- end}}
{{if .ArtifactID}}**ArtifactId:** {{ .ArtifactID }}{{- end}}
{{if .Group}}**Group:** {{ .Group }}{{- end}}
{{if .Version}}**Version:** {{ .Version }}{{- end}}
{{if .PackageURL}}**Package URL:** {{ .PackageURL }}{{- end}}
{{if .PublishDate}}**Publishing date:** {{.PublishDate }}{{- end}}
## Description
{{ .Description }}
---
{{.Footer}}
`
// ToMarkdown creates a vulnerability in markdown format which can be used in GitHub issues
func (v *VulnerabilityReport) ToMarkdown() ([]byte, error) {
funcMap := template.FuncMap{
"date": func(t time.Time) string {
return t.Format("2006-01-02")
},
"title": func(s string) string {
caser := cases.Title(language.AmericanEnglish)
return caser.String(s)
},
}
// only fill with orchestrator information if orchestrator can be identified properly
if provider, err := orchestrator.NewOrchestratorSpecificConfigProvider(); err == nil {
// only add information if not yet provided
if len(v.CommitID) == 0 {
v.CommitID = provider.GetCommit()
}
if len(v.PipelineLink) == 0 {
v.PipelineLink = provider.GetJobURL()
v.PipelineName = provider.GetJobName()
}
}
md := []byte{}
tmpl, err := template.New("report").Funcs(funcMap).Parse(vulnerabilityMdTemplate)
if err != nil {
return md, fmt.Errorf("failed to create markdown issue template: %w", err)
}
buf := new(bytes.Buffer)
err = tmpl.Execute(buf, v)
if err != nil {
return md, fmt.Errorf("failed to execute markdown issue template: %w", err)
}
md = buf.Bytes()
return md, nil
}
// ScanReport defines the elements of a scan report used by various scan steps
type ScanReport struct {
StepName string `json:"stepName"`
ReportTitle string `json:"title"`
Subheaders []Subheader `json:"subheaders"`
Overview []OverviewRow `json:"overview"`
FurtherInfo string `json:"furtherInfo"`
ReportTime time.Time `json:"reportTime"`
DetailTable ScanDetailTable `json:"detailTable"`
SuccessfulScan bool `json:"successfulScan"`
}
// ScanDetailTable defines a table containing scan result details
type ScanDetailTable struct {
Headers []string `json:"headers"`
Rows []ScanRow `json:"rows"`
WithCounter bool `json:"withCounter"`
CounterHeader string `json:"counterHeader"`
NoRowsMessage string `json:"noRowsMessage"`
}
// ScanRow defines one row of a scan result table
type ScanRow struct {
Columns []ScanCell `json:"columns"`
}
// AddColumn adds a column to a dedicated ScanRow
func (s *ScanRow) AddColumn(content interface{}, style ColumnStyle) {
if s.Columns == nil {
s.Columns = []ScanCell{}
}
s.Columns = append(s.Columns, ScanCell{Content: fmt.Sprint(content), Style: style})
}
// ScanCell defines one column of a scan result table
type ScanCell struct {
Content string `json:"content"`
Style ColumnStyle `json:"style"`
}
// ColumnStyle defines style for a specific column
type ColumnStyle int
// enum for style types
const (
Green = iota + 1
Yellow
Red
Grey
Black
)
func (c ColumnStyle) String() string {
return [...]string{"", "green-cell", "yellow-cell", "red-cell", "grey-cell", "black-cell"}[c]
}
// OverviewRow defines a row in the report's overview section
// it can consist of a description and some details where the details can have a style attached
type OverviewRow struct {
Description string `json:"description"`
Details string `json:"details,omitempty"`
Style ColumnStyle `json:"style,omitempty"`
}
// Subheader defines a dedicated sub header in a report
type Subheader struct {
Description string `json:"text"`
Details string `json:"details,omitempty"`
}
// AddSubHeader adds a sub header to the report containing of a text/title plus optional details
func (s *ScanReport) AddSubHeader(header, details string) {
s.Subheaders = append(s.Subheaders, Subheader{Description: header, Details: details})
}
// StepReportDirectory specifies the default directory for markdown reports which can later be collected by step pipelineCreateSummary
const StepReportDirectory = ".pipeline/stepReports"
// ToJSON returns the report in JSON format
func (s *ScanReport) ToJSON() ([]byte, error) {
return json.Marshal(s)
}
// ToTxt up to now returns the report in JSON format
func (s ScanReport) ToTxt() string {
txt, _ := s.ToJSON()
return string(txt)
}
const reportHTMLTemplate = `<!DOCTYPE html>
<html>
<head>
<title>{{.Title}}</title>
<style type="text/css">
body {
font-family: Arial, Verdana;
}
table {
border-collapse: collapse;
}
div.code {
font-family: "Courier New", "Lucida Console";
}
th {
border-top: 1px solid #ddd;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
border-right: 1px solid #ddd;
}
tr:nth-child(even) {
background-color: #f2f2f2;
}
.bold {
font-weight: bold;
}
.green{
color: olivedrab;
}
.red{
color: orangered;
}
.nobullets {
list-style-type:none;
padding-left: 0;
padding-bottom: 0;
margin: 0;
}
.green-cell {
background-color: #e1f5a9;
padding: 5px
}
.yellow-cell {
background-color: #ffff99;
padding: 5px
}
.red-cell {
background-color: #ffe5e5;
padding: 5px
}
.grey-cell{
background-color: rgba(212, 212, 212, 0.7);
padding: 5px;
}
.black-cell{
background-color: rgba(0, 0, 0, 0.75);
padding: 5px;
}
</style>
</head>
<body>
<h1>{{.Title}}</h1>
<h2>
<span>
{{range $s := .Subheaders}}
{{- $s.Description}}: {{$s.Details}}<br />
{{end -}}
</span>
</h2>
<div>
<h3>
{{range $o := .Overview}}
{{- drawOverviewRow $o}}<br />
{{end -}}
</h3>
<span>{{.FurtherInfo}}</span>
</div>
<p>Snapshot taken: {{reportTime .ReportTime}}</p>
<table>
<tr>
{{if .DetailTable.WithCounter}}<th>{{.DetailTable.CounterHeader}}</th>{{end}}
{{- range $h := .DetailTable.Headers}}
<th>{{$h}}</th>
{{- end}}
</tr>
{{range $i, $r := .DetailTable.Rows}}
<tr>
{{if $.DetailTable.WithCounter}}<td>{{inc $i}}</td>{{end}}
{{- range $c := $r.Columns}}
{{drawCell $c}}
{{- end}}
</tr>
{{else}}
<tr><td colspan="{{columnCount .DetailTable}}">{{.DetailTable.NoRowsMessage}}</td></tr>
{{- end}}
</table>
</body>
</html>
`
// ToHTML creates a HTML version of the report
func (s *ScanReport) ToHTML() ([]byte, error) {
funcMap := template.FuncMap{
"inc": func(i int) int {
return i + 1
},
"reportTime": func(currentTime time.Time) string {
return currentTime.Format("Jan 02, 2006 - 15:04:05 MST")
},
"columnCount": tableColumnCount,
"drawCell": drawCell,
"drawOverviewRow": drawOverviewRow,
}
report := []byte{}
tmpl, err := template.New("report").Funcs(funcMap).Parse(reportHTMLTemplate)
if err != nil {
return report, errors.Wrap(err, "failed to create HTML report template")
}
buf := new(bytes.Buffer)
err = tmpl.Execute(buf, s)
if err != nil {
return report, errors.Wrap(err, "failed to execute HTML report template")
}
return buf.Bytes(), nil
}
const reportMdTemplate = `## {{if .SuccessfulScan}}:white_check_mark:{{else}}:x:{{end}} {{.Title}}
<table>
{{range $s := .Subheaders -}}
<tr><td><b>{{- $s.Description}}:</b></td><td>{{$s.Details}}</td></tr>
{{- end}}
{{range $o := .Overview -}}
{{drawOverviewRow $o}}
{{- end}}
</table>
{{.FurtherInfo}}
Snapshot taken: <i>{{reportTime .ReportTime}}</i>
{{if shouldDrawTable .DetailTable -}}
<details><summary><i>{{.Title}} details:</i></summary>
<p>
<table>
<tr>
{{if .DetailTable.WithCounter}}<th>{{.DetailTable.CounterHeader}}</th>{{end}}
{{- range $h := .DetailTable.Headers}}
<th>{{$h}}</th>
{{- end}}
</tr>
{{range $i, $r := .DetailTable.Rows}}
<tr>
{{if $.DetailTable.WithCounter}}<td>{{inc $i}}</td>{{end}}
{{- range $c := $r.Columns}}
{{drawCell $c}}
{{- end}}
</tr>
{{else}}
<tr><td colspan="{{columnCount .DetailTable}}">{{.DetailTable.NoRowsMessage}}</td></tr>
{{- end}}
</table>
</p>
</details>
{{ end }}
`
// Title returns the title of the report
func (s ScanReport) Title() string {
return s.ReportTitle
}
// ToMarkdown creates a markdown version of the report content
func (s ScanReport) ToMarkdown() ([]byte, error) {
funcMap := template.FuncMap{
"columnCount": tableColumnCount,
"drawCell": drawCell,
"shouldDrawTable": shouldDrawTable,
"inc": func(i int) int {
return i + 1
},
"reportTime": func(currentTime time.Time) string {
return currentTime.Format("Jan 02, 2006 - 15:04:05 MST")
},
"drawOverviewRow": drawOverviewRowMarkdown,
}
report := []byte{}
tmpl, err := template.New("report").Funcs(funcMap).Parse(reportMdTemplate)
if err != nil {
return report, errors.Wrap(err, "failed to create Markdown report template")
}
buf := new(bytes.Buffer)
err = tmpl.Execute(buf, s)
if err != nil {
return report, errors.Wrap(err, "failed to execute Markdown report template")
}
return buf.Bytes(), nil
}
func tableColumnCount(scanDetails ScanDetailTable) int {
colCount := len(scanDetails.Headers)
if scanDetails.WithCounter {
colCount++
}
return colCount
}
func drawCell(cell ScanCell) string {
if cell.Style > 0 {
return fmt.Sprintf(`<td class="%v">%v</td>`, cell.Style, cell.Content)
}
return fmt.Sprintf(`<td>%v</td>`, cell.Content)
}
func shouldDrawTable(table ScanDetailTable) bool {
if len(table.Headers) > 0 {
return true
}
return false
}
func drawOverviewRow(row OverviewRow) string {
// so far accept only accept max. two columns for overview table: description and content
if len(row.Details) == 0 {
return row.Description
}
// ToDo: allow styling of details
return fmt.Sprintf("%v: %v", row.Description, row.Details)
}
func drawOverviewRowMarkdown(row OverviewRow) string {
// so far accept only accept max. two columns for overview table: description and content
if len(row.Details) == 0 {
return row.Description
}
// ToDo: allow styling of details
return fmt.Sprintf("<tr><td>%v:</td><td>%v</td></tr>", row.Description, row.Details)
}