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 }