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  }