package reporting import ( "bytes" "encoding/json" "fmt" "text/template" "time" "github.com/pkg/errors" ) // ScanReport defines the elements of a scan report used by various scan steps type ScanReport struct { StepName string `json:"stepName"` Title 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) } const reportHTMLTemplate = ` {{.Title}}

{{.Title}}

{{range $s := .Subheaders}} {{- $s.Description}}: {{$s.Details}}
{{end -}}

{{range $o := .Overview}} {{- drawOverviewRow $o}}
{{end -}}

{{.FurtherInfo}}

Snapshot taken: {{reportTime .ReportTime}}

{{if .DetailTable.WithCounter}}{{end}} {{- range $h := .DetailTable.Headers}} {{- end}} {{range $i, $r := .DetailTable.Rows}} {{if $.DetailTable.WithCounter}}{{end}} {{- range $c := $r.Columns}} {{drawCell $c}} {{- end}} {{else}} {{- end}}
{{.DetailTable.CounterHeader}}{{$h}}
{{inc $i}}
{{.DetailTable.NoRowsMessage}}
` // 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}} {{range $s := .Subheaders -}} {{- end}} {{range $o := .Overview -}} {{drawOverviewRow $o}} {{- end}}
{{- $s.Description}}:{{$s.Details}}
{{.FurtherInfo}} Snapshot taken: {{reportTime .ReportTime}} {{if shouldDrawTable .DetailTable -}}
{{.Title}} details:

{{if .DetailTable.WithCounter}}{{end}} {{- range $h := .DetailTable.Headers}} {{- end}} {{range $i, $r := .DetailTable.Rows}} {{if $.DetailTable.WithCounter}}{{end}} {{- range $c := $r.Columns}} {{drawCell $c}} {{- end}} {{else}} {{- end}}
{{.DetailTable.CounterHeader}}{{$h}}
{{inc $i}}
{{.DetailTable.NoRowsMessage}}

{{ end }} ` // 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(`%v`, cell.Style, cell.Content) } return fmt.Sprintf(`%v`, 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("%v:%v", row.Description, row.Details) }