github.com/jaylevin/jenkins-library@v1.230.4/pkg/whitesource/reporting.go (about) 1 package whitesource 2 3 import ( 4 "crypto/sha1" 5 "encoding/json" 6 "fmt" 7 "path/filepath" 8 "sort" 9 "strings" 10 "time" 11 12 "github.com/SAP/jenkins-library/pkg/format" 13 "github.com/SAP/jenkins-library/pkg/log" 14 "github.com/SAP/jenkins-library/pkg/piperutils" 15 "github.com/SAP/jenkins-library/pkg/reporting" 16 "github.com/pkg/errors" 17 ) 18 19 // CreateCustomVulnerabilityReport creates a vulnerability ScanReport to be used for uploading into various sinks 20 func CreateCustomVulnerabilityReport(productName string, scan *Scan, alerts *[]Alert, cvssSeverityLimit float64) reporting.ScanReport { 21 severe, _ := CountSecurityVulnerabilities(alerts, cvssSeverityLimit) 22 23 // sort according to vulnerability severity 24 sort.Slice(*alerts, func(i, j int) bool { 25 return vulnerabilityScore((*alerts)[i]) > vulnerabilityScore((*alerts)[j]) 26 }) 27 28 projectNames := scan.ScannedProjectNames() 29 30 scanReport := reporting.ScanReport{ 31 ReportTitle: "WhiteSource Security Vulnerability Report", 32 Subheaders: []reporting.Subheader{ 33 {Description: "WhiteSource product name", Details: productName}, 34 {Description: "Filtered project names", Details: strings.Join(projectNames, ", ")}, 35 }, 36 Overview: []reporting.OverviewRow{ 37 {Description: "Total number of vulnerabilities", Details: fmt.Sprint(len((*alerts)))}, 38 {Description: "Total number of high/critical vulnerabilities with CVSS score >= 7.0", Details: fmt.Sprint(severe)}, 39 }, 40 SuccessfulScan: severe == 0, 41 ReportTime: time.Now(), 42 } 43 44 detailTable := reporting.ScanDetailTable{ 45 NoRowsMessage: "No publicly known vulnerabilities detected", 46 Headers: []string{ 47 "Date", 48 "CVE", 49 "CVSS Score", 50 "CVSS Version", 51 "Project", 52 "Library file name", 53 "Library group ID", 54 "Library artifact ID", 55 "Library version", 56 "Description", 57 "Top fix", 58 }, 59 WithCounter: true, 60 CounterHeader: "Entry #", 61 } 62 63 for _, alert := range *alerts { 64 var score float64 65 var scoreStyle reporting.ColumnStyle = reporting.Yellow 66 if isSevereVulnerability(alert, cvssSeverityLimit) { 67 scoreStyle = reporting.Red 68 } 69 var cveVersion string 70 if alert.Vulnerability.CVSS3Score > 0 { 71 score = alert.Vulnerability.CVSS3Score 72 cveVersion = "v3" 73 } else { 74 score = alert.Vulnerability.Score 75 cveVersion = "v2" 76 } 77 78 var topFix string 79 emptyFix := Fix{} 80 if alert.Vulnerability.TopFix != emptyFix { 81 topFix = fmt.Sprintf(`%v<br>%v<br><a href="%v">%v</a>}"`, alert.Vulnerability.TopFix.Message, alert.Vulnerability.TopFix.FixResolution, alert.Vulnerability.TopFix.URL, alert.Vulnerability.TopFix.URL) 82 } 83 84 row := reporting.ScanRow{} 85 row.AddColumn(alert.Vulnerability.PublishDate, 0) 86 row.AddColumn(fmt.Sprintf(`<a href="%v">%v</a>`, alert.Vulnerability.URL, alert.Vulnerability.Name), 0) 87 row.AddColumn(score, scoreStyle) 88 row.AddColumn(cveVersion, 0) 89 row.AddColumn(alert.Project, 0) 90 row.AddColumn(alert.Library.Filename, 0) 91 row.AddColumn(alert.Library.GroupID, 0) 92 row.AddColumn(alert.Library.ArtifactID, 0) 93 row.AddColumn(alert.Library.Version, 0) 94 row.AddColumn(alert.Vulnerability.Description, 0) 95 row.AddColumn(topFix, 0) 96 97 detailTable.Rows = append(detailTable.Rows, row) 98 } 99 scanReport.DetailTable = detailTable 100 101 return scanReport 102 } 103 104 // CountSecurityVulnerabilities counts the security vulnerabilities above severityLimit 105 func CountSecurityVulnerabilities(alerts *[]Alert, cvssSeverityLimit float64) (int, int) { 106 severeVulnerabilities := 0 107 for _, alert := range *alerts { 108 if isSevereVulnerability(alert, cvssSeverityLimit) { 109 severeVulnerabilities++ 110 } 111 } 112 113 nonSevereVulnerabilities := len(*alerts) - severeVulnerabilities 114 return severeVulnerabilities, nonSevereVulnerabilities 115 } 116 117 func isSevereVulnerability(alert Alert, cvssSeverityLimit float64) bool { 118 119 if vulnerabilityScore(alert) >= cvssSeverityLimit && cvssSeverityLimit >= 0 { 120 return true 121 } 122 return false 123 } 124 125 func vulnerabilityScore(alert Alert) float64 { 126 if alert.Vulnerability.CVSS3Score > 0 { 127 return alert.Vulnerability.CVSS3Score 128 } 129 return alert.Vulnerability.Score 130 } 131 132 // ReportSha creates a SHA unique to the WS product and scan to be used as part of the report filename 133 func ReportSha(productName string, scan *Scan) string { 134 reportShaData := []byte(productName + "," + strings.Join(scan.ScannedProjectNames(), ",")) 135 return fmt.Sprintf("%x", sha1.Sum(reportShaData)) 136 } 137 138 // WriteCustomVulnerabilityReports creates an HTML and a JSON format file based on the alerts brought up by the scan 139 func WriteCustomVulnerabilityReports(productName string, scan *Scan, scanReport reporting.ScanReport, utils piperutils.FileUtils) ([]piperutils.Path, error) { 140 reportPaths := []piperutils.Path{} 141 142 // ignore templating errors since template is in our hands and issues will be detected with the automated tests 143 htmlReport, _ := scanReport.ToHTML() 144 if err := utils.MkdirAll(ReportsDirectory, 0777); err != nil { 145 return reportPaths, errors.Wrapf(err, "failed to create report directory") 146 } 147 htmlReportPath := filepath.Join(ReportsDirectory, "piper_whitesource_vulnerability_report.html") 148 if err := utils.FileWrite(htmlReportPath, htmlReport, 0666); err != nil { 149 log.SetErrorCategory(log.ErrorConfiguration) 150 return reportPaths, errors.Wrapf(err, "failed to write html report") 151 } 152 reportPaths = append(reportPaths, piperutils.Path{Name: "WhiteSource Vulnerability Report", Target: htmlReportPath}) 153 154 // JSON reports are used by step pipelineCreateSummary in order to e.g. prepare an issue creation in GitHub 155 // ignore JSON errors since structure is in our hands 156 jsonReport, _ := scanReport.ToJSON() 157 if exists, _ := utils.DirExists(reporting.StepReportDirectory); !exists { 158 err := utils.MkdirAll(reporting.StepReportDirectory, 0777) 159 if err != nil { 160 return reportPaths, errors.Wrap(err, "failed to create step reporting directory") 161 } 162 } 163 if err := utils.FileWrite(filepath.Join(reporting.StepReportDirectory, fmt.Sprintf("whitesourceExecuteScan_oss_%v.json", ReportSha(productName, scan))), jsonReport, 0666); err != nil { 164 return reportPaths, errors.Wrapf(err, "failed to write json report") 165 } 166 // we do not add the json report to the overall list of reports for now, 167 // since it is just an intermediary report used as input for later 168 // and there does not seem to be real benefit in archiving it. 169 170 return reportPaths, nil 171 } 172 173 // Creates a SARIF result from the Alerts that were brought up by the scan 174 func CreateSarifResultFile(scan *Scan, alerts *[]Alert) *format.SARIF { 175 //Now, we handle the sarif 176 log.Entry().Debug("Creating SARIF file for data transfer") 177 var sarif format.SARIF 178 sarif.Schema = "https://docs.oasis-open.org/sarif/sarif/v2.1.0/cos02/schemas/sarif-schema-2.1.0.json" 179 sarif.Version = "2.1.0" 180 var wsRun format.Runs 181 sarif.Runs = append(sarif.Runs, wsRun) 182 183 //handle the tool object 184 tool := *new(format.Tool) 185 tool.Driver = *new(format.Driver) 186 tool.Driver.Name = scan.AgentName 187 tool.Driver.Version = scan.AgentVersion 188 tool.Driver.InformationUri = "https://whitesource.atlassian.net/wiki/spaces/WD/pages/804814917/Unified+Agent+Overview" 189 190 // Handle results/vulnerabilities 191 for i := 0; i < len(*alerts); i++ { 192 alert := (*alerts)[i] 193 result := *new(format.Results) 194 id := fmt.Sprintf("%v/%v/%v", alert.Type, alert.Vulnerability.Name, alert.Library.ArtifactID) 195 log.Entry().Debugf("Transforming alert %v into SARIF format", id) 196 result.RuleID = id 197 result.Level = transformToLevel(alert.Vulnerability.Severity, alert.Vulnerability.CVSS3Severity) 198 result.RuleIndex = i //Seems very abstract 199 result.Message = new(format.Message) 200 result.Message.Text = alert.Vulnerability.Description 201 artLoc := new(format.ArtifactLocation) 202 artLoc.Index = 0 203 artLoc.URI = alert.Library.Filename 204 result.AnalysisTarget = artLoc 205 location := format.Location{PhysicalLocation: format.PhysicalLocation{ArtifactLocation: format.ArtifactLocation{URI: alert.Library.Filename}}} 206 result.Locations = append(result.Locations, location) 207 //TODO add audit and tool related information, maybe fortifyCategory needs to become more general 208 //result.Properties = new(format.SarifProperties) 209 //result.Properties.ToolSeverity 210 //result.Properties.ToolAuditMessage 211 212 sarifRule := *new(format.SarifRule) 213 sarifRule.ID = id 214 sd := new(format.Message) 215 sd.Text = fmt.Sprintf("%v Package %v", alert.Vulnerability.Name, alert.Library.ArtifactID) 216 sarifRule.ShortDescription = sd 217 fd := new(format.Message) 218 fd.Text = alert.Vulnerability.Description 219 sarifRule.FullDescription = fd 220 defaultConfig := new(format.DefaultConfiguration) 221 defaultConfig.Level = transformToLevel(alert.Vulnerability.Severity, alert.Vulnerability.CVSS3Severity) 222 sarifRule.DefaultConfiguration = defaultConfig 223 sarifRule.HelpURI = alert.Vulnerability.URL 224 markdown, _ := alert.ToMarkdown() 225 sarifRule.Help = new(format.Help) 226 sarifRule.Help.Text = alert.ToTxt() 227 sarifRule.Help.Markdown = string(markdown) 228 229 ruleProp := *new(format.SarifRuleProperties) 230 ruleProp.Tags = append(ruleProp.Tags, alert.Type) 231 ruleProp.Tags = append(ruleProp.Tags, alert.Description) 232 ruleProp.Tags = append(ruleProp.Tags, alert.Library.ArtifactID) 233 ruleProp.Precision = "very-high" 234 sarifRule.Properties = &ruleProp 235 236 //Finalize: append the result and the rule 237 sarif.Runs[0].Results = append(sarif.Runs[0].Results, result) 238 tool.Driver.Rules = append(tool.Driver.Rules, sarifRule) 239 } 240 //Finalize: tool 241 sarif.Runs[0].Tool = tool 242 243 return &sarif 244 } 245 246 func transformToLevel(cvss2severity, cvss3severity string) string { 247 switch cvss3severity { 248 case "low": 249 return "warning" 250 case "medium": 251 return "warning" 252 case "high": 253 return "error" 254 } 255 switch cvss2severity { 256 case "low": 257 return "warning" 258 case "medium": 259 return "warning" 260 case "high": 261 return "error" 262 } 263 return "none" 264 } 265 266 // WriteSarifFile write a JSON sarif format file for upload into e.g. GCP 267 func WriteSarifFile(sarif *format.SARIF, utils piperutils.FileUtils) ([]piperutils.Path, error) { 268 reportPaths := []piperutils.Path{} 269 270 // ignore templating errors since template is in our hands and issues will be detected with the automated tests 271 sarifReport, errorMarshall := json.Marshal(sarif) 272 if errorMarshall != nil { 273 return reportPaths, errors.Wrapf(errorMarshall, "failed to marshall SARIF json file") 274 } 275 if err := utils.MkdirAll(ReportsDirectory, 0777); err != nil { 276 return reportPaths, errors.Wrapf(err, "failed to create report directory") 277 } 278 sarifReportPath := filepath.Join(ReportsDirectory, "piper_whitesource_vulnerability.sarif") 279 if err := utils.FileWrite(sarifReportPath, sarifReport, 0666); err != nil { 280 log.SetErrorCategory(log.ErrorConfiguration) 281 return reportPaths, errors.Wrapf(err, "failed to write SARIF file") 282 } 283 reportPaths = append(reportPaths, piperutils.Path{Name: "WhiteSource Vulnerability SARIF file", Target: sarifReportPath}) 284 285 return reportPaths, nil 286 }