github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/pkg/reporting/pullRequestReport.go (about)

     1  package reporting
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"sort"
    11  	"strconv"
    12  	"strings"
    13  	"text/template"
    14  
    15  	"github.com/SAP/jenkins-library/pkg/log"
    16  )
    17  
    18  // Components - for parsing from file
    19  type Components []Component
    20  
    21  type Component struct {
    22  	ComponentName                  string                         `json:"componentName"`
    23  	ComponentVersion               string                         `json:"versionName"`
    24  	ComponentIdentifier            string                         `json:"componentIdentifier"`
    25  	ViolatingPolicyNames           []string                       `json:"violatingPolicyNames"`
    26  	PolicyViolationVulnerabilities []PolicyViolationVulnerability `json:"policyViolationVulnerabilities"`
    27  	PolicyViolationLicenses        []PolicyViolationLicense       `json:"policyViolationLicenses"`
    28  	WarningMessage                 string                         `json:"warningMessage"`
    29  	ErrorMessage                   string                         `json:"errorMessage"`
    30  }
    31  
    32  type PolicyViolationVulnerability struct {
    33  	Name                 string   `json:"name"`
    34  	ViolatingPolicyNames []string `json:"ViolatingPolicyNames"`
    35  	WarningMessage       string   `json:"warningMessage"`
    36  	ErrorMessage         string   `json:"errorMessage"`
    37  	Meta                 Meta     `json:"_meta"`
    38  }
    39  
    40  type PolicyViolationLicense struct {
    41  	LicenseName          string   `json:"licenseName"`
    42  	ViolatingPolicyNames []string `json:"violatingPolicyNames"`
    43  	Meta                 Meta     `json:"_meta"`
    44  }
    45  
    46  type Meta struct {
    47  	Href string `json:"href"`
    48  }
    49  
    50  // RapidScanReport - for commenting to pull requests
    51  type RapidScanReport struct {
    52  	Success bool
    53  
    54  	ExecutedTime string
    55  
    56  	MainTableHeaders []string
    57  	MainTableValues  [][]string
    58  
    59  	VulnerabilitiesTable []Vulnerabilities
    60  	LicensesTable        []Licenses
    61  	OtherViolationsTable []OtherViolations
    62  }
    63  
    64  type Vulnerabilities struct {
    65  	PolicyViolationName string
    66  	Values              []Vulnerability
    67  }
    68  
    69  type Vulnerability struct {
    70  	VulnerabilityID    string
    71  	VulnerabilityScore string
    72  	ComponentName      string
    73  	VulnerabilityHref  string
    74  }
    75  
    76  type Licenses struct {
    77  	PolicyViolationName string
    78  	Values              []License
    79  }
    80  
    81  type License struct {
    82  	LicenseName   string
    83  	ComponentName string
    84  	LicenseHref   string
    85  }
    86  
    87  type OtherViolations struct {
    88  	PolicyViolationName string
    89  	Values              []OtherViolation
    90  }
    91  
    92  type OtherViolation struct {
    93  	ComponentName string
    94  }
    95  
    96  const rapidReportMdTemplate = `
    97  ## {{if .Success}}:heavy_check_mark: OSS related checks passed successfully
    98   ### :clipboard: OSS related checks executed by Black Duck - rapid scan passed successfully.
    99   <a href="https://community.synopsys.com/s/document-item?bundleId=integrations-detect&topicId=downloadingandrunning%2Frapidscan.html&_LANG=enus"><h3>RAPID SCAN</h3> </a>
   100  
   101  {{else}} :x: OSS related checks failed
   102   ### :clipboard: Policies violated by added OSS components
   103   <table>
   104   <tr>{{range $s := .MainTableHeaders -}}<td><b>{{$s}}</b></td>{{- end}}</tr>
   105   {{range $s := .MainTableValues -}}<tr>{{range $s1 := $s }}<td>{{$s1}}</td>{{- end}}</tr>
   106   {{- end}}
   107   </table>
   108  
   109  {{range $index := .VulnerabilitiesTable -}}
   110  <details><summary>
   111  {{$len := len $index.Values}}
   112  {{if le $len 1}} <h3> {{$len}} Policy Violation of {{$index.PolicyViolationName}}</h3>
   113  {{else}}<h3> {{$len}} Policy Violations of {{$index.PolicyViolationName}} </h3> {{end}}
   114  </summary>
   115  	<table>
   116  		<tr><td><b>Vulnerability ID</b></td><td><b>Vulnerability Score</b></td><td><b>Component Name</b></td></tr>
   117  		{{range $value := $index.Values -}}
   118  			<tr>
   119  			<td> <a href="{{$value.VulnerabilityHref}}"> {{$value.VulnerabilityID}} </a> </td><td>{{$value.VulnerabilityScore}}</td><td>{{$value.ComponentName}}</td>
   120  			</tr>
   121  		{{end -}}
   122  	</table>
   123  </details>
   124  {{end -}}
   125  {{range $index := .LicensesTable -}}
   126  <details><summary>
   127  {{$len := len $index.Values}}
   128  {{if le $len 1}} <h3> {{$len}} Policy Violation of {{$index.PolicyViolationName}}</h3>
   129  {{else}}<h3> {{$len}} Policy Violations of {{$index.PolicyViolationName}} </h3> {{end}}
   130  </summary>
   131  	<table>
   132  		<tr><td><b>License Name</b></td><td><b>Component Name</b></td></tr>
   133  		{{range $value := $index.Values -}}
   134  			<tr><td> <a href="{{$value.LicenseHref}}"> {{$value.LicenseName}} </a> </td><td>{{$value.ComponentName}}</td></tr>
   135  		{{end -}}
   136  	</table>
   137  </details>
   138  {{end -}}
   139  {{range $index := .OtherViolationsTable -}}
   140  <details><summary>
   141  {{$len := len $index.Values}}
   142  {{if le $len 1}} <h3> {{$len}} Policy Violation of {{$index.PolicyViolationName}}</h3>
   143  {{else}}<h3> {{$len}} Policy Violations of {{$index.PolicyViolationName}} </h3> {{end}}
   144  </summary>
   145  	<table>
   146  		<tr><td><b>Component Name</b></td></tr>
   147  		{{range $value := $index.Values -}}
   148  			<tr><td>{{$value.ComponentName}}</td></tr>
   149  		{{end -}}
   150  	</table>
   151  </details>
   152  {{end -}}
   153  {{end}}
   154  `
   155  
   156  // RapidScanResult reads result of Rapid scan from generated file
   157  func RapidScanResult(dir string) (string, error) {
   158  	components, removeDir, err := findAndReadJsonFile(dir)
   159  	if err != nil {
   160  		return "", err
   161  	}
   162  	if components == nil {
   163  		return "", errors.New("couldn't parse info from file")
   164  	}
   165  
   166  	buf, err := createMarkdownReport(components)
   167  	if err != nil {
   168  		return "", err
   169  	}
   170  
   171  	err = os.RemoveAll(removeDir)
   172  	if err != nil {
   173  		log.Entry().Error("Couldn't remove report file", err)
   174  	}
   175  
   176  	return buf.String(), nil
   177  }
   178  
   179  type Files []os.DirEntry
   180  
   181  // findLastCreatedDir finds last created directory
   182  func findLastCreatedDir(directories []os.DirEntry) os.DirEntry {
   183  	lastCreatedDir := directories[0]
   184  	for _, dir := range directories {
   185  		if dir.Name() > lastCreatedDir.Name() {
   186  			lastCreatedDir = dir
   187  		}
   188  	}
   189  	return lastCreatedDir
   190  }
   191  
   192  // findAndReadJsonFile find file BlackDuck_DeveloperMode_Result.json generated by detectExecuteStep and read it
   193  func findAndReadJsonFile(dir string) (*Components, string, error) {
   194  	var err error
   195  	filePath := dir + "/runs"
   196  	allFiles, err := os.ReadDir(filePath)
   197  	if err != nil {
   198  		return nil, "", err
   199  	}
   200  	if allFiles == nil {
   201  		return nil, "", errors.New("no report files")
   202  	}
   203  	lastDir := findLastCreatedDir(allFiles)
   204  	removeDir := filePath + "/" + lastDir.Name()
   205  	filePath = filePath + "/" + lastDir.Name() + "/scan"
   206  	files, err := os.ReadDir(filePath)
   207  	if err != nil {
   208  		return nil, "", err
   209  	}
   210  	if files == nil {
   211  		return nil, "", errors.New("no report files")
   212  	}
   213  
   214  	for _, file := range files {
   215  		if !file.IsDir() && strings.HasSuffix(file.Name(), "BlackDuck_DeveloperMode_Result.json") {
   216  			var result Components
   217  			jsonFile, err := os.Open(filePath + "/" + file.Name())
   218  			if err != nil {
   219  				return nil, "", err
   220  			}
   221  			fileBody, err := io.ReadAll(jsonFile)
   222  			if err != nil {
   223  				return nil, "", err
   224  			}
   225  			err = json.Unmarshal(fileBody, &result)
   226  			if err != nil {
   227  				return nil, "", err
   228  			}
   229  			err = jsonFile.Close()
   230  			if err != nil {
   231  				log.Entry().Error(fmt.Sprintf("Couldn't close %s", jsonFile.Name()), err)
   232  			}
   233  			return &result, removeDir, nil
   234  		}
   235  	}
   236  
   237  	return nil, "", nil
   238  }
   239  
   240  // createMarkdownReport creates markdown report to upload it as GitHub PR comment
   241  func createMarkdownReport(components *Components) (*bytes.Buffer, error) {
   242  	// preparing report
   243  	var scanReport RapidScanReport
   244  	scanReport.Success = true
   245  
   246  	// getting reports to maps
   247  	allPolicyViolationsMapUsed := make(map[string]bool)
   248  	countPolicyViolationComponent := make(map[string]map[string]int)
   249  	vulnerabilities := make(map[string][]Vulnerability)
   250  	licenses := make(map[string][]License)
   251  	otherViolations := make(map[string][]OtherViolation)
   252  	componentNames := make([]string, len(*components))
   253  
   254  	for idx, component := range *components {
   255  		componentName := component.ComponentName + " " + component.ComponentVersion + " (" + component.ComponentIdentifier + ")"
   256  		componentNames[idx] = componentName
   257  
   258  		// for others
   259  		for _, policyViolationName := range component.ViolatingPolicyNames {
   260  			if !allPolicyViolationsMapUsed[policyViolationName] {
   261  				allPolicyViolationsMapUsed[policyViolationName] = true
   262  				scanReport.MainTableHeaders = append(scanReport.MainTableHeaders, policyViolationName)
   263  			}
   264  			if countPolicyViolationComponent[policyViolationName] == nil {
   265  				countPolicyViolationComponent[policyViolationName] = make(map[string]int)
   266  			}
   267  			msg := component.ErrorMessage + " " + component.WarningMessage
   268  			if strings.Contains(msg, policyViolationName) {
   269  				countPolicyViolationComponent[policyViolationName][componentName]++
   270  				otherViolations[policyViolationName] = append(otherViolations[policyViolationName], OtherViolation{ComponentName: componentName})
   271  			}
   272  		}
   273  
   274  		// for Vulnerabilities
   275  		for _, policyVulnerability := range component.PolicyViolationVulnerabilities {
   276  			for _, policyViolationName := range policyVulnerability.ViolatingPolicyNames {
   277  				if countPolicyViolationComponent[policyViolationName] == nil {
   278  					countPolicyViolationComponent[policyViolationName] = make(map[string]int)
   279  				}
   280  				countPolicyViolationComponent[policyViolationName][componentName]++
   281  				vulnerabilities[policyViolationName] = append(vulnerabilities[policyViolationName],
   282  					Vulnerability{
   283  						VulnerabilityID:    policyVulnerability.Name,
   284  						VulnerabilityHref:  policyVulnerability.Meta.Href,
   285  						VulnerabilityScore: getScore(policyVulnerability.ErrorMessage, "score") + " " + getScore(policyVulnerability.ErrorMessage, "severity"),
   286  						ComponentName:      componentName,
   287  					})
   288  			}
   289  		}
   290  
   291  		// for Licenses
   292  		for _, policyViolationLicense := range component.PolicyViolationLicenses {
   293  			for _, policyViolationName := range policyViolationLicense.ViolatingPolicyNames {
   294  				if countPolicyViolationComponent[policyViolationName] == nil {
   295  					countPolicyViolationComponent[policyViolationName] = make(map[string]int)
   296  				}
   297  				countPolicyViolationComponent[policyViolationName][componentName]++
   298  				licenses[policyViolationName] = append(licenses[policyViolationName],
   299  					License{
   300  						LicenseName:   policyViolationLicense.LicenseName,
   301  						LicenseHref:   policyViolationLicense.Meta.Href + "/license-terms",
   302  						ComponentName: componentName,
   303  					})
   304  			}
   305  		}
   306  	}
   307  
   308  	if scanReport.MainTableHeaders != nil && componentNames != nil {
   309  		scanReport.Success = false
   310  
   311  		// MainTable sort & copy
   312  		sort.Strings(scanReport.MainTableHeaders)
   313  		sort.Strings(componentNames)
   314  		scanReport.MainTableHeaders = append([]string{"Component name"}, scanReport.MainTableHeaders...)
   315  		for i := range componentNames {
   316  			scanReport.MainTableValues = append(scanReport.MainTableValues, []string{})
   317  			scanReport.MainTableValues[i] = append(scanReport.MainTableValues[i], componentNames[i])
   318  			for j := 1; j < len(scanReport.MainTableHeaders); j++ {
   319  				policyV := scanReport.MainTableHeaders[j]
   320  				comp := componentNames[i]
   321  				count := strconv.Itoa(countPolicyViolationComponent[policyV][comp])
   322  				scanReport.MainTableValues[i] = append(scanReport.MainTableValues[i], count)
   323  			}
   324  		}
   325  
   326  		// VulnerabilitiesTable sort & copy
   327  		for key := range vulnerabilities {
   328  			item := vulnerabilities[key]
   329  			sort.Slice(item, func(i, j int) bool {
   330  				return scoreLogicSort(item[i].VulnerabilityScore, item[j].VulnerabilityScore)
   331  			})
   332  			scanReport.VulnerabilitiesTable = append(scanReport.VulnerabilitiesTable, Vulnerabilities{
   333  				PolicyViolationName: key,
   334  				Values:              item,
   335  			})
   336  		}
   337  		sort.Slice(scanReport.VulnerabilitiesTable, func(i, j int) bool {
   338  			return scanReport.VulnerabilitiesTable[i].PolicyViolationName < scanReport.VulnerabilitiesTable[j].PolicyViolationName
   339  		})
   340  
   341  		// LicensesTable sort & copy
   342  		for key := range licenses {
   343  			item := licenses[key]
   344  			sort.Slice(item, func(i, j int) bool {
   345  				if item[i].LicenseName < item[j].LicenseName {
   346  					return true
   347  				}
   348  				if item[i].LicenseName > item[j].LicenseName {
   349  					return false
   350  				}
   351  				return item[i].ComponentName < item[j].ComponentName
   352  			})
   353  			scanReport.LicensesTable = append(scanReport.LicensesTable, Licenses{
   354  				PolicyViolationName: key,
   355  				Values:              item,
   356  			})
   357  		}
   358  		sort.Slice(scanReport.LicensesTable, func(i, j int) bool {
   359  			return scanReport.LicensesTable[i].PolicyViolationName < scanReport.LicensesTable[j].PolicyViolationName
   360  		})
   361  
   362  		// OtherViolationsTable sort & copy
   363  		for key := range otherViolations {
   364  			item := otherViolations[key]
   365  			sort.Slice(item, func(i, j int) bool {
   366  				return item[i].ComponentName < item[j].ComponentName
   367  			})
   368  			scanReport.OtherViolationsTable = append(scanReport.OtherViolationsTable, OtherViolations{
   369  				PolicyViolationName: key,
   370  				Values:              item,
   371  			})
   372  		}
   373  		sort.Slice(scanReport.OtherViolationsTable, func(i, j int) bool {
   374  			return scanReport.OtherViolationsTable[i].PolicyViolationName < scanReport.OtherViolationsTable[j].PolicyViolationName
   375  		})
   376  	}
   377  
   378  	tmpl, err := template.New("report").Parse(rapidReportMdTemplate)
   379  	if err != nil {
   380  		return nil, errors.New("failed to create Markdown report template err:" + err.Error())
   381  	}
   382  	buf := new(bytes.Buffer)
   383  	err = tmpl.Execute(buf, scanReport)
   384  	if err != nil {
   385  		return nil, errors.New("failed to create Markdown report template err:" + err.Error())
   386  	}
   387  
   388  	return buf, nil
   389  }
   390  
   391  // getScore extracts score or severity from error message
   392  func getScore(message, key string) string {
   393  	indx := strings.Index(message, key)
   394  	if indx == -1 {
   395  		return ""
   396  	}
   397  	var result string
   398  	var notFirstSpace bool
   399  	for _, s := range message[indx+len(key):] {
   400  		if s == ' ' && notFirstSpace {
   401  			break
   402  		}
   403  		notFirstSpace = true
   404  		result = result + string(s)
   405  	}
   406  	return strings.Trim(result, " ")
   407  }
   408  
   409  // scoreLogicSort sorts two scores
   410  func scoreLogicSort(iStr, jStr string) bool {
   411  	if strings.Contains(iStr, "10.0") {
   412  		return true
   413  	} else if strings.Contains(jStr, "10.0") {
   414  		return false
   415  	}
   416  	if iStr >= jStr {
   417  		return true
   418  	}
   419  	return false
   420  }