2021-01-25 12:09:03 +02:00
|
|
|
package reporting
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2021-03-19 12:10:08 +02:00
|
|
|
"encoding/json"
|
2021-01-25 12:09:03 +02:00
|
|
|
"fmt"
|
|
|
|
"text/template"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
)
|
|
|
|
|
|
|
|
// ScanReport defines the elements of a scan report used by various scan steps
|
|
|
|
type ScanReport struct {
|
2021-02-02 15:36:40 +02:00
|
|
|
StepName string `json:"stepName"`
|
|
|
|
Title string `json:"title"`
|
2021-02-10 17:18:00 +02:00
|
|
|
Subheaders []Subheader `json:"subheaders"`
|
|
|
|
Overview []OverviewRow `json:"overview"`
|
2021-02-02 15:36:40 +02:00
|
|
|
FurtherInfo string `json:"furtherInfo"`
|
|
|
|
ReportTime time.Time `json:"reportTime"`
|
|
|
|
DetailTable ScanDetailTable `json:"detailTable"`
|
|
|
|
SuccessfulScan bool `json:"successfulScan"`
|
2021-01-25 12:09:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// ScanDetailTable defines a table containing scan result details
|
|
|
|
type ScanDetailTable struct {
|
2021-02-02 15:36:40 +02:00
|
|
|
Headers []string `json:"headers"`
|
|
|
|
Rows []ScanRow `json:"rows"`
|
|
|
|
WithCounter bool `json:"withCounter"`
|
|
|
|
CounterHeader string `json:"counterHeader"`
|
|
|
|
NoRowsMessage string `json:"noRowsMessage"`
|
2021-01-25 12:09:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// ScanRow defines one row of a scan result table
|
|
|
|
type ScanRow struct {
|
2021-02-02 15:36:40 +02:00
|
|
|
Columns []ScanCell `json:"columns"`
|
2021-01-25 12:09:03 +02:00
|
|
|
}
|
|
|
|
|
2021-02-10 17:18:00 +02:00
|
|
|
// 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})
|
|
|
|
}
|
|
|
|
|
2021-01-25 12:09:03 +02:00
|
|
|
// ScanCell defines one column of a scan result table
|
|
|
|
type ScanCell struct {
|
2021-02-02 15:36:40 +02:00
|
|
|
Content string `json:"content"`
|
|
|
|
Style ColumnStyle `json:"style"`
|
2021-01-25 12:09:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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]
|
|
|
|
}
|
|
|
|
|
2021-02-10 17:18:00 +02:00
|
|
|
// 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})
|
|
|
|
}
|
|
|
|
|
2021-03-19 12:10:08 +02:00
|
|
|
//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)
|
|
|
|
}
|
2021-02-10 17:18:00 +02:00
|
|
|
|
2021-01-25 12:09:03 +02:00
|
|
|
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}}
|
2021-02-10 17:18:00 +02:00
|
|
|
{{- $s.Description}}: {{$s.Details}}<br />
|
2021-01-25 12:09:03 +02:00
|
|
|
{{end -}}
|
|
|
|
</span>
|
|
|
|
</h2>
|
|
|
|
<div>
|
|
|
|
<h3>
|
|
|
|
{{range $o := .Overview}}
|
2021-02-10 17:18:00 +02:00
|
|
|
{{- drawOverviewRow $o}}<br />
|
2021-01-25 12:09:03 +02:00
|
|
|
{{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")
|
|
|
|
},
|
2021-02-10 17:18:00 +02:00
|
|
|
"columnCount": tableColumnCount,
|
|
|
|
"drawCell": drawCell,
|
|
|
|
"drawOverviewRow": drawOverviewRow,
|
2021-01-25 12:09:03 +02:00
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2021-05-11 08:01:02 +02:00
|
|
|
const reportMdTemplate = `## {{if .SuccessfulScan}}:white_check_mark:{{else}}:x:{{end}} {{.Title}}
|
2021-01-25 12:09:03 +02:00
|
|
|
|
2021-04-15 07:45:06 +02:00
|
|
|
<table>
|
|
|
|
{{range $s := .Subheaders -}}
|
|
|
|
<tr><td><b>{{- $s.Description}}:</b></td><td>{{$s.Details}}</td></tr>
|
|
|
|
{{- end}}
|
2021-01-25 12:09:03 +02:00
|
|
|
|
2021-04-15 07:45:06 +02:00
|
|
|
{{range $o := .Overview -}}
|
|
|
|
{{drawOverviewRow $o}}
|
|
|
|
{{- end}}
|
|
|
|
</table>
|
2021-01-25 12:09:03 +02:00
|
|
|
|
2021-02-10 17:18:00 +02:00
|
|
|
{{.FurtherInfo}}
|
2021-01-25 12:09:03 +02:00
|
|
|
|
2021-04-15 07:45:06 +02:00
|
|
|
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>
|
2021-02-10 17:18:00 +02:00
|
|
|
</p>
|
2021-04-15 07:45:06 +02:00
|
|
|
</details>
|
|
|
|
{{ end }}
|
|
|
|
|
|
|
|
`
|
2021-02-02 15:36:40 +02:00
|
|
|
|
2021-02-10 17:18:00 +02:00
|
|
|
// ToMarkdown creates a markdown version of the report content
|
|
|
|
func (s *ScanReport) ToMarkdown() ([]byte, error) {
|
|
|
|
funcMap := template.FuncMap{
|
2021-04-15 07:45:06 +02:00
|
|
|
"columnCount": tableColumnCount,
|
|
|
|
"drawCell": drawCell,
|
|
|
|
"shouldDrawTable": shouldDrawTable,
|
|
|
|
"inc": func(i int) int {
|
|
|
|
return i + 1
|
|
|
|
},
|
2021-02-10 17:18:00 +02:00
|
|
|
"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
|
2021-01-25 12:09:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
2021-02-10 17:18:00 +02:00
|
|
|
|
2021-04-15 07:45:06 +02:00
|
|
|
func shouldDrawTable(table ScanDetailTable) bool {
|
|
|
|
if len(table.Headers) > 0 {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2021-02-10 17:18:00 +02:00
|
|
|
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
|
2021-04-15 07:45:06 +02:00
|
|
|
return fmt.Sprintf("<tr><td>%v:</td><td>%v</td></tr>", row.Description, row.Details)
|
2021-02-10 17:18:00 +02:00
|
|
|
}
|