mirror of
https://github.com/SAP/jenkins-library.git
synced 2024-12-12 10:55:20 +02:00
f4fbf0f1ed
Co-authored-by: akram8008 <900658008.akram@email.com> Co-authored-by: Christopher Fenner <26137398+CCFenner@users.noreply.github.com> Co-authored-by: Andrei Kireev <andrei.kireev@sap.com> Co-authored-by: ffeldmann <f.feldmann@sap.com> Co-authored-by: sumeet patil <sumeet.patil@sap.com>
421 lines
13 KiB
Go
421 lines
13 KiB
Go
package reporting
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"github.com/SAP/jenkins-library/pkg/log"
|
|
)
|
|
|
|
// Components - for parsing from file
|
|
type Components []Component
|
|
|
|
type Component struct {
|
|
ComponentName string `json:"componentName"`
|
|
ComponentVersion string `json:"versionName"`
|
|
ComponentIdentifier string `json:"componentIdentifier"`
|
|
ViolatingPolicyNames []string `json:"violatingPolicyNames"`
|
|
PolicyViolationVulnerabilities []PolicyViolationVulnerability `json:"policyViolationVulnerabilities"`
|
|
PolicyViolationLicenses []PolicyViolationLicense `json:"policyViolationLicenses"`
|
|
WarningMessage string `json:"warningMessage"`
|
|
ErrorMessage string `json:"errorMessage"`
|
|
}
|
|
|
|
type PolicyViolationVulnerability struct {
|
|
Name string `json:"name"`
|
|
ViolatingPolicyNames []string `json:"ViolatingPolicyNames"`
|
|
WarningMessage string `json:"warningMessage"`
|
|
ErrorMessage string `json:"errorMessage"`
|
|
Meta Meta `json:"_meta"`
|
|
}
|
|
|
|
type PolicyViolationLicense struct {
|
|
LicenseName string `json:"licenseName"`
|
|
ViolatingPolicyNames []string `json:"violatingPolicyNames"`
|
|
Meta Meta `json:"_meta"`
|
|
}
|
|
|
|
type Meta struct {
|
|
Href string `json:"href"`
|
|
}
|
|
|
|
// RapidScanReport - for commenting to pull requests
|
|
type RapidScanReport struct {
|
|
Success bool
|
|
|
|
ExecutedTime string
|
|
|
|
MainTableHeaders []string
|
|
MainTableValues [][]string
|
|
|
|
VulnerabilitiesTable []Vulnerabilities
|
|
LicensesTable []Licenses
|
|
OtherViolationsTable []OtherViolations
|
|
}
|
|
|
|
type Vulnerabilities struct {
|
|
PolicyViolationName string
|
|
Values []Vulnerability
|
|
}
|
|
|
|
type Vulnerability struct {
|
|
VulnerabilityID string
|
|
VulnerabilityScore string
|
|
ComponentName string
|
|
VulnerabilityHref string
|
|
}
|
|
|
|
type Licenses struct {
|
|
PolicyViolationName string
|
|
Values []License
|
|
}
|
|
|
|
type License struct {
|
|
LicenseName string
|
|
ComponentName string
|
|
LicenseHref string
|
|
}
|
|
|
|
type OtherViolations struct {
|
|
PolicyViolationName string
|
|
Values []OtherViolation
|
|
}
|
|
|
|
type OtherViolation struct {
|
|
ComponentName string
|
|
}
|
|
|
|
const rapidReportMdTemplate = `
|
|
## {{if .Success}}:heavy_check_mark: OSS related checks passed successfully
|
|
### :clipboard: OSS related checks executed by Black Duck - rapid scan passed successfully.
|
|
<a href="https://community.synopsys.com/s/document-item?bundleId=integrations-detect&topicId=downloadingandrunning%2Frapidscan.html&_LANG=enus"><h3>RAPID SCAN</h3> </a>
|
|
|
|
{{else}} :x: OSS related checks failed
|
|
### :clipboard: Policies violated by added OSS components
|
|
<table>
|
|
<tr>{{range $s := .MainTableHeaders -}}<td><b>{{$s}}</b></td>{{- end}}</tr>
|
|
{{range $s := .MainTableValues -}}<tr>{{range $s1 := $s }}<td>{{$s1}}</td>{{- end}}</tr>
|
|
{{- end}}
|
|
</table>
|
|
|
|
{{range $index := .VulnerabilitiesTable -}}
|
|
<details><summary>
|
|
{{$len := len $index.Values}}
|
|
{{if le $len 1}} <h3> {{$len}} Policy Violation of {{$index.PolicyViolationName}}</h3>
|
|
{{else}}<h3> {{$len}} Policy Violations of {{$index.PolicyViolationName}} </h3> {{end}}
|
|
</summary>
|
|
<table>
|
|
<tr><td><b>Vulnerability ID</b></td><td><b>Vulnerability Score</b></td><td><b>Component Name</b></td></tr>
|
|
{{range $value := $index.Values -}}
|
|
<tr>
|
|
<td> <a href="{{$value.VulnerabilityHref}}"> {{$value.VulnerabilityID}} </a> </td><td>{{$value.VulnerabilityScore}}</td><td>{{$value.ComponentName}}</td>
|
|
</tr>
|
|
{{end -}}
|
|
</table>
|
|
</details>
|
|
{{end -}}
|
|
{{range $index := .LicensesTable -}}
|
|
<details><summary>
|
|
{{$len := len $index.Values}}
|
|
{{if le $len 1}} <h3> {{$len}} Policy Violation of {{$index.PolicyViolationName}}</h3>
|
|
{{else}}<h3> {{$len}} Policy Violations of {{$index.PolicyViolationName}} </h3> {{end}}
|
|
</summary>
|
|
<table>
|
|
<tr><td><b>License Name</b></td><td><b>Component Name</b></td></tr>
|
|
{{range $value := $index.Values -}}
|
|
<tr><td> <a href="{{$value.LicenseHref}}"> {{$value.LicenseName}} </a> </td><td>{{$value.ComponentName}}</td></tr>
|
|
{{end -}}
|
|
</table>
|
|
</details>
|
|
{{end -}}
|
|
{{range $index := .OtherViolationsTable -}}
|
|
<details><summary>
|
|
{{$len := len $index.Values}}
|
|
{{if le $len 1}} <h3> {{$len}} Policy Violation of {{$index.PolicyViolationName}}</h3>
|
|
{{else}}<h3> {{$len}} Policy Violations of {{$index.PolicyViolationName}} </h3> {{end}}
|
|
</summary>
|
|
<table>
|
|
<tr><td><b>Component Name</b></td></tr>
|
|
{{range $value := $index.Values -}}
|
|
<tr><td>{{$value.ComponentName}}</td></tr>
|
|
{{end -}}
|
|
</table>
|
|
</details>
|
|
{{end -}}
|
|
{{end}}
|
|
`
|
|
|
|
// RapidScanResult reads result of Rapid scan from generated file
|
|
func RapidScanResult(dir string) (string, error) {
|
|
components, removeDir, err := findAndReadJsonFile(dir)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if components == nil {
|
|
return "", errors.New("couldn't parse info from file")
|
|
}
|
|
|
|
buf, err := createMarkdownReport(components)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
err = os.RemoveAll(removeDir)
|
|
if err != nil {
|
|
log.Entry().Error("Couldn't remove report file", err)
|
|
}
|
|
|
|
return buf.String(), nil
|
|
}
|
|
|
|
type Files []os.DirEntry
|
|
|
|
// findLastCreatedDir finds last created directory
|
|
func findLastCreatedDir(directories []os.DirEntry) os.DirEntry {
|
|
lastCreatedDir := directories[0]
|
|
for _, dir := range directories {
|
|
if dir.Name() > lastCreatedDir.Name() {
|
|
lastCreatedDir = dir
|
|
}
|
|
}
|
|
return lastCreatedDir
|
|
}
|
|
|
|
// findAndReadJsonFile find file BlackDuck_DeveloperMode_Result.json generated by detectExecuteStep and read it
|
|
func findAndReadJsonFile(dir string) (*Components, string, error) {
|
|
var err error
|
|
filePath := dir + "/runs"
|
|
allFiles, err := os.ReadDir(filePath)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
if allFiles == nil {
|
|
return nil, "", errors.New("no report files")
|
|
}
|
|
lastDir := findLastCreatedDir(allFiles)
|
|
removeDir := filePath + "/" + lastDir.Name()
|
|
filePath = filePath + "/" + lastDir.Name() + "/scan"
|
|
files, err := os.ReadDir(filePath)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
if files == nil {
|
|
return nil, "", errors.New("no report files")
|
|
}
|
|
|
|
for _, file := range files {
|
|
if !file.IsDir() && strings.HasSuffix(file.Name(), "BlackDuck_DeveloperMode_Result.json") {
|
|
var result Components
|
|
jsonFile, err := os.Open(filePath + "/" + file.Name())
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
fileBody, err := io.ReadAll(jsonFile)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
err = json.Unmarshal(fileBody, &result)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
err = jsonFile.Close()
|
|
if err != nil {
|
|
log.Entry().Error(fmt.Sprintf("Couldn't close %s", jsonFile.Name()), err)
|
|
}
|
|
return &result, removeDir, nil
|
|
}
|
|
}
|
|
|
|
return nil, "", nil
|
|
}
|
|
|
|
// createMarkdownReport creates markdown report to upload it as GitHub PR comment
|
|
func createMarkdownReport(components *Components) (*bytes.Buffer, error) {
|
|
// preparing report
|
|
var scanReport RapidScanReport
|
|
scanReport.Success = true
|
|
|
|
// getting reports to maps
|
|
allPolicyViolationsMapUsed := make(map[string]bool)
|
|
countPolicyViolationComponent := make(map[string]map[string]int)
|
|
vulnerabilities := make(map[string][]Vulnerability)
|
|
licenses := make(map[string][]License)
|
|
otherViolations := make(map[string][]OtherViolation)
|
|
componentNames := make([]string, len(*components))
|
|
|
|
for idx, component := range *components {
|
|
componentName := component.ComponentName + " " + component.ComponentVersion + " (" + component.ComponentIdentifier + ")"
|
|
componentNames[idx] = componentName
|
|
|
|
// for others
|
|
for _, policyViolationName := range component.ViolatingPolicyNames {
|
|
if !allPolicyViolationsMapUsed[policyViolationName] {
|
|
allPolicyViolationsMapUsed[policyViolationName] = true
|
|
scanReport.MainTableHeaders = append(scanReport.MainTableHeaders, policyViolationName)
|
|
}
|
|
if countPolicyViolationComponent[policyViolationName] == nil {
|
|
countPolicyViolationComponent[policyViolationName] = make(map[string]int)
|
|
}
|
|
msg := component.ErrorMessage + " " + component.WarningMessage
|
|
if strings.Contains(msg, policyViolationName) {
|
|
countPolicyViolationComponent[policyViolationName][componentName]++
|
|
otherViolations[policyViolationName] = append(otherViolations[policyViolationName], OtherViolation{ComponentName: componentName})
|
|
}
|
|
}
|
|
|
|
// for Vulnerabilities
|
|
for _, policyVulnerability := range component.PolicyViolationVulnerabilities {
|
|
for _, policyViolationName := range policyVulnerability.ViolatingPolicyNames {
|
|
if countPolicyViolationComponent[policyViolationName] == nil {
|
|
countPolicyViolationComponent[policyViolationName] = make(map[string]int)
|
|
}
|
|
countPolicyViolationComponent[policyViolationName][componentName]++
|
|
vulnerabilities[policyViolationName] = append(vulnerabilities[policyViolationName],
|
|
Vulnerability{
|
|
VulnerabilityID: policyVulnerability.Name,
|
|
VulnerabilityHref: policyVulnerability.Meta.Href,
|
|
VulnerabilityScore: getScore(policyVulnerability.ErrorMessage, "score") + " " + getScore(policyVulnerability.ErrorMessage, "severity"),
|
|
ComponentName: componentName,
|
|
})
|
|
}
|
|
}
|
|
|
|
// for Licenses
|
|
for _, policyViolationLicense := range component.PolicyViolationLicenses {
|
|
for _, policyViolationName := range policyViolationLicense.ViolatingPolicyNames {
|
|
if countPolicyViolationComponent[policyViolationName] == nil {
|
|
countPolicyViolationComponent[policyViolationName] = make(map[string]int)
|
|
}
|
|
countPolicyViolationComponent[policyViolationName][componentName]++
|
|
licenses[policyViolationName] = append(licenses[policyViolationName],
|
|
License{
|
|
LicenseName: policyViolationLicense.LicenseName,
|
|
LicenseHref: policyViolationLicense.Meta.Href + "/license-terms",
|
|
ComponentName: componentName,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
if scanReport.MainTableHeaders != nil && componentNames != nil {
|
|
scanReport.Success = false
|
|
|
|
// MainTable sort & copy
|
|
sort.Strings(scanReport.MainTableHeaders)
|
|
sort.Strings(componentNames)
|
|
scanReport.MainTableHeaders = append([]string{"Component name"}, scanReport.MainTableHeaders...)
|
|
for i := range componentNames {
|
|
scanReport.MainTableValues = append(scanReport.MainTableValues, []string{})
|
|
scanReport.MainTableValues[i] = append(scanReport.MainTableValues[i], componentNames[i])
|
|
for j := 1; j < len(scanReport.MainTableHeaders); j++ {
|
|
policyV := scanReport.MainTableHeaders[j]
|
|
comp := componentNames[i]
|
|
count := strconv.Itoa(countPolicyViolationComponent[policyV][comp])
|
|
scanReport.MainTableValues[i] = append(scanReport.MainTableValues[i], count)
|
|
}
|
|
}
|
|
|
|
// VulnerabilitiesTable sort & copy
|
|
for key := range vulnerabilities {
|
|
item := vulnerabilities[key]
|
|
sort.Slice(item, func(i, j int) bool {
|
|
return scoreLogicSort(item[i].VulnerabilityScore, item[j].VulnerabilityScore)
|
|
})
|
|
scanReport.VulnerabilitiesTable = append(scanReport.VulnerabilitiesTable, Vulnerabilities{
|
|
PolicyViolationName: key,
|
|
Values: item,
|
|
})
|
|
}
|
|
sort.Slice(scanReport.VulnerabilitiesTable, func(i, j int) bool {
|
|
return scanReport.VulnerabilitiesTable[i].PolicyViolationName < scanReport.VulnerabilitiesTable[j].PolicyViolationName
|
|
})
|
|
|
|
// LicensesTable sort & copy
|
|
for key := range licenses {
|
|
item := licenses[key]
|
|
sort.Slice(item, func(i, j int) bool {
|
|
if item[i].LicenseName < item[j].LicenseName {
|
|
return true
|
|
}
|
|
if item[i].LicenseName > item[j].LicenseName {
|
|
return false
|
|
}
|
|
return item[i].ComponentName < item[j].ComponentName
|
|
})
|
|
scanReport.LicensesTable = append(scanReport.LicensesTable, Licenses{
|
|
PolicyViolationName: key,
|
|
Values: item,
|
|
})
|
|
}
|
|
sort.Slice(scanReport.LicensesTable, func(i, j int) bool {
|
|
return scanReport.LicensesTable[i].PolicyViolationName < scanReport.LicensesTable[j].PolicyViolationName
|
|
})
|
|
|
|
// OtherViolationsTable sort & copy
|
|
for key := range otherViolations {
|
|
item := otherViolations[key]
|
|
sort.Slice(item, func(i, j int) bool {
|
|
return item[i].ComponentName < item[j].ComponentName
|
|
})
|
|
scanReport.OtherViolationsTable = append(scanReport.OtherViolationsTable, OtherViolations{
|
|
PolicyViolationName: key,
|
|
Values: item,
|
|
})
|
|
}
|
|
sort.Slice(scanReport.OtherViolationsTable, func(i, j int) bool {
|
|
return scanReport.OtherViolationsTable[i].PolicyViolationName < scanReport.OtherViolationsTable[j].PolicyViolationName
|
|
})
|
|
}
|
|
|
|
tmpl, err := template.New("report").Parse(rapidReportMdTemplate)
|
|
if err != nil {
|
|
return nil, errors.New("failed to create Markdown report template err:" + err.Error())
|
|
}
|
|
buf := new(bytes.Buffer)
|
|
err = tmpl.Execute(buf, scanReport)
|
|
if err != nil {
|
|
return nil, errors.New("failed to create Markdown report template err:" + err.Error())
|
|
}
|
|
|
|
return buf, nil
|
|
}
|
|
|
|
// getScore extracts score or severity from error message
|
|
func getScore(message, key string) string {
|
|
indx := strings.Index(message, key)
|
|
if indx == -1 {
|
|
return ""
|
|
}
|
|
var result string
|
|
var notFirstSpace bool
|
|
for _, s := range message[indx+len(key):] {
|
|
if s == ' ' && notFirstSpace {
|
|
break
|
|
}
|
|
notFirstSpace = true
|
|
result = result + string(s)
|
|
}
|
|
return strings.Trim(result, " ")
|
|
}
|
|
|
|
// scoreLogicSort sorts two scores
|
|
func scoreLogicSort(iStr, jStr string) bool {
|
|
if strings.Contains(iStr, "10.0") {
|
|
return true
|
|
} else if strings.Contains(jStr, "10.0") {
|
|
return false
|
|
}
|
|
if iStr >= jStr {
|
|
return true
|
|
}
|
|
return false
|
|
}
|