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.

RAPID SCAN

{{else}} :x: OSS related checks failed ### :clipboard: Policies violated by added OSS components {{range $s := .MainTableHeaders -}}{{- end}} {{range $s := .MainTableValues -}}{{range $s1 := $s }}{{- end}} {{- end}}
{{$s}}
{{$s1}}
{{range $index := .VulnerabilitiesTable -}}
{{$len := len $index.Values}} {{if le $len 1}}

{{$len}} Policy Violation of {{$index.PolicyViolationName}}

{{else}}

{{$len}} Policy Violations of {{$index.PolicyViolationName}}

{{end}}
{{range $value := $index.Values -}} {{end -}}
Vulnerability IDVulnerability ScoreComponent Name
{{$value.VulnerabilityID}} {{$value.VulnerabilityScore}}{{$value.ComponentName}}
{{end -}} {{range $index := .LicensesTable -}}
{{$len := len $index.Values}} {{if le $len 1}}

{{$len}} Policy Violation of {{$index.PolicyViolationName}}

{{else}}

{{$len}} Policy Violations of {{$index.PolicyViolationName}}

{{end}}
{{range $value := $index.Values -}} {{end -}}
License NameComponent Name
{{$value.LicenseName}} {{$value.ComponentName}}
{{end -}} {{range $index := .OtherViolationsTable -}}
{{$len := len $index.Values}} {{if le $len 1}}

{{$len}} Policy Violation of {{$index.PolicyViolationName}}

{{else}}

{{$len}} Policy Violations of {{$index.PolicyViolationName}}

{{end}}
{{range $value := $index.Values -}} {{end -}}
Component Name
{{$value.ComponentName}}
{{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 }