package whitesource
import (
"crypto/sha1"
"encoding/json"
"fmt"
"path/filepath"
"sort"
"strings"
"time"
"github.com/SAP/jenkins-library/pkg/format"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/SAP/jenkins-library/pkg/piperutils"
"github.com/SAP/jenkins-library/pkg/reporting"
"github.com/pkg/errors"
)
// CreateCustomVulnerabilityReport creates a vulnerability ScanReport to be used for uploading into various sinks
func CreateCustomVulnerabilityReport(productName string, scan *Scan, alerts *[]Alert, cvssSeverityLimit float64) reporting.ScanReport {
severe, _ := CountSecurityVulnerabilities(alerts, cvssSeverityLimit)
// sort according to vulnerability severity
sort.Slice(*alerts, func(i, j int) bool {
return vulnerabilityScore((*alerts)[i]) > vulnerabilityScore((*alerts)[j])
})
projectNames := scan.ScannedProjectNames()
scanReport := reporting.ScanReport{
ReportTitle: "WhiteSource Security Vulnerability Report",
Subheaders: []reporting.Subheader{
{Description: "WhiteSource product name", Details: productName},
{Description: "Filtered project names", Details: strings.Join(projectNames, ", ")},
},
Overview: []reporting.OverviewRow{
{Description: "Total number of vulnerabilities", Details: fmt.Sprint(len((*alerts)))},
{Description: "Total number of high/critical vulnerabilities with CVSS score >= 7.0", Details: fmt.Sprint(severe)},
},
SuccessfulScan: severe == 0,
ReportTime: time.Now(),
}
detailTable := reporting.ScanDetailTable{
NoRowsMessage: "No publicly known vulnerabilities detected",
Headers: []string{
"Date",
"CVE",
"CVSS Score",
"CVSS Version",
"Project",
"Library file name",
"Library group ID",
"Library artifact ID",
"Library version",
"Description",
"Top fix",
},
WithCounter: true,
CounterHeader: "Entry #",
}
for _, alert := range *alerts {
var score float64
var scoreStyle reporting.ColumnStyle = reporting.Yellow
if isSevereVulnerability(alert, cvssSeverityLimit) {
scoreStyle = reporting.Red
}
var cveVersion string
if alert.Vulnerability.CVSS3Score > 0 {
score = alert.Vulnerability.CVSS3Score
cveVersion = "v3"
} else {
score = alert.Vulnerability.Score
cveVersion = "v2"
}
var topFix string
emptyFix := Fix{}
if alert.Vulnerability.TopFix != emptyFix {
topFix = fmt.Sprintf(`%v
%v
%v}"`, alert.Vulnerability.TopFix.Message, alert.Vulnerability.TopFix.FixResolution, alert.Vulnerability.TopFix.URL, alert.Vulnerability.TopFix.URL)
}
row := reporting.ScanRow{}
row.AddColumn(alert.Vulnerability.PublishDate, 0)
row.AddColumn(fmt.Sprintf(`%v`, alert.Vulnerability.URL, alert.Vulnerability.Name), 0)
row.AddColumn(score, scoreStyle)
row.AddColumn(cveVersion, 0)
row.AddColumn(alert.Project, 0)
row.AddColumn(alert.Library.Filename, 0)
row.AddColumn(alert.Library.GroupID, 0)
row.AddColumn(alert.Library.ArtifactID, 0)
row.AddColumn(alert.Library.Version, 0)
row.AddColumn(alert.Vulnerability.Description, 0)
row.AddColumn(topFix, 0)
detailTable.Rows = append(detailTable.Rows, row)
}
scanReport.DetailTable = detailTable
return scanReport
}
// CountSecurityVulnerabilities counts the security vulnerabilities above severityLimit
func CountSecurityVulnerabilities(alerts *[]Alert, cvssSeverityLimit float64) (int, int) {
severeVulnerabilities := 0
for _, alert := range *alerts {
if isSevereVulnerability(alert, cvssSeverityLimit) {
severeVulnerabilities++
}
}
nonSevereVulnerabilities := len(*alerts) - severeVulnerabilities
return severeVulnerabilities, nonSevereVulnerabilities
}
func isSevereVulnerability(alert Alert, cvssSeverityLimit float64) bool {
if vulnerabilityScore(alert) >= cvssSeverityLimit && cvssSeverityLimit >= 0 {
return true
}
return false
}
func vulnerabilityScore(alert Alert) float64 {
if alert.Vulnerability.CVSS3Score > 0 {
return alert.Vulnerability.CVSS3Score
}
return alert.Vulnerability.Score
}
// ReportSha creates a SHA unique to the WS product and scan to be used as part of the report filename
func ReportSha(productName string, scan *Scan) string {
reportShaData := []byte(productName + "," + strings.Join(scan.ScannedProjectNames(), ","))
return fmt.Sprintf("%x", sha1.Sum(reportShaData))
}
// WriteCustomVulnerabilityReports creates an HTML and a JSON format file based on the alerts brought up by the scan
func WriteCustomVulnerabilityReports(productName string, scan *Scan, scanReport reporting.ScanReport, utils piperutils.FileUtils) ([]piperutils.Path, error) {
reportPaths := []piperutils.Path{}
// ignore templating errors since template is in our hands and issues will be detected with the automated tests
htmlReport, _ := scanReport.ToHTML()
if err := utils.MkdirAll(ReportsDirectory, 0777); err != nil {
return reportPaths, errors.Wrapf(err, "failed to create report directory")
}
htmlReportPath := filepath.Join(ReportsDirectory, "piper_whitesource_vulnerability_report.html")
if err := utils.FileWrite(htmlReportPath, htmlReport, 0666); err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return reportPaths, errors.Wrapf(err, "failed to write html report")
}
reportPaths = append(reportPaths, piperutils.Path{Name: "WhiteSource Vulnerability Report", Target: htmlReportPath})
// JSON reports are used by step pipelineCreateSummary in order to e.g. prepare an issue creation in GitHub
// ignore JSON errors since structure is in our hands
jsonReport, _ := scanReport.ToJSON()
if exists, _ := utils.DirExists(reporting.StepReportDirectory); !exists {
err := utils.MkdirAll(reporting.StepReportDirectory, 0777)
if err != nil {
return reportPaths, errors.Wrap(err, "failed to create step reporting directory")
}
}
if err := utils.FileWrite(filepath.Join(reporting.StepReportDirectory, fmt.Sprintf("whitesourceExecuteScan_oss_%v.json", ReportSha(productName, scan))), jsonReport, 0666); err != nil {
return reportPaths, errors.Wrapf(err, "failed to write json report")
}
// we do not add the json report to the overall list of reports for now,
// since it is just an intermediary report used as input for later
// and there does not seem to be real benefit in archiving it.
return reportPaths, nil
}
// Creates a SARIF result from the Alerts that were brought up by the scan
func CreateSarifResultFile(scan *Scan, alerts *[]Alert) *format.SARIF {
//Now, we handle the sarif
log.Entry().Debug("Creating SARIF file for data transfer")
var sarif format.SARIF
sarif.Schema = "https://docs.oasis-open.org/sarif/sarif/v2.1.0/cos02/schemas/sarif-schema-2.1.0.json"
sarif.Version = "2.1.0"
var wsRun format.Runs
sarif.Runs = append(sarif.Runs, wsRun)
//handle the tool object
tool := *new(format.Tool)
tool.Driver = *new(format.Driver)
tool.Driver.Name = scan.AgentName
tool.Driver.Version = scan.AgentVersion
tool.Driver.InformationUri = "https://whitesource.atlassian.net/wiki/spaces/WD/pages/804814917/Unified+Agent+Overview"
// Handle results/vulnerabilities
for i := 0; i < len(*alerts); i++ {
alert := (*alerts)[i]
result := *new(format.Results)
id := fmt.Sprintf("%v/%v/%v", alert.Type, alert.Vulnerability.Name, alert.Library.ArtifactID)
log.Entry().Debugf("Transforming alert %v into SARIF format", id)
result.RuleID = id
result.Level = transformToLevel(alert.Vulnerability.Severity, alert.Vulnerability.CVSS3Severity)
result.RuleIndex = i //Seems very abstract
result.Message = new(format.Message)
result.Message.Text = alert.Vulnerability.Description
artLoc := new(format.ArtifactLocation)
artLoc.Index = 0
artLoc.URI = alert.Library.Filename
result.AnalysisTarget = artLoc
location := format.Location{PhysicalLocation: format.PhysicalLocation{ArtifactLocation: format.ArtifactLocation{URI: alert.Library.Filename}}}
result.Locations = append(result.Locations, location)
//TODO add audit and tool related information, maybe fortifyCategory needs to become more general
//result.Properties = new(format.SarifProperties)
//result.Properties.ToolSeverity
//result.Properties.ToolAuditMessage
sarifRule := *new(format.SarifRule)
sarifRule.ID = id
sd := new(format.Message)
sd.Text = fmt.Sprintf("%v Package %v", alert.Vulnerability.Name, alert.Library.ArtifactID)
sarifRule.ShortDescription = sd
fd := new(format.Message)
fd.Text = alert.Vulnerability.Description
sarifRule.FullDescription = fd
defaultConfig := new(format.DefaultConfiguration)
defaultConfig.Level = transformToLevel(alert.Vulnerability.Severity, alert.Vulnerability.CVSS3Severity)
sarifRule.DefaultConfiguration = defaultConfig
sarifRule.HelpURI = alert.Vulnerability.URL
markdown, _ := alert.ToMarkdown()
sarifRule.Help = new(format.Help)
sarifRule.Help.Text = alert.ToTxt()
sarifRule.Help.Markdown = string(markdown)
ruleProp := *new(format.SarifRuleProperties)
ruleProp.Tags = append(ruleProp.Tags, alert.Type)
ruleProp.Tags = append(ruleProp.Tags, alert.Description)
ruleProp.Tags = append(ruleProp.Tags, alert.Library.ArtifactID)
ruleProp.Precision = "very-high"
sarifRule.Properties = &ruleProp
//Finalize: append the result and the rule
sarif.Runs[0].Results = append(sarif.Runs[0].Results, result)
tool.Driver.Rules = append(tool.Driver.Rules, sarifRule)
}
//Finalize: tool
sarif.Runs[0].Tool = tool
return &sarif
}
func transformToLevel(cvss2severity, cvss3severity string) string {
switch cvss3severity {
case "low":
return "warning"
case "medium":
return "warning"
case "high":
return "error"
}
switch cvss2severity {
case "low":
return "warning"
case "medium":
return "warning"
case "high":
return "error"
}
return "none"
}
// WriteSarifFile write a JSON sarif format file for upload into e.g. GCP
func WriteSarifFile(sarif *format.SARIF, utils piperutils.FileUtils) ([]piperutils.Path, error) {
reportPaths := []piperutils.Path{}
// ignore templating errors since template is in our hands and issues will be detected with the automated tests
sarifReport, errorMarshall := json.Marshal(sarif)
if errorMarshall != nil {
return reportPaths, errors.Wrapf(errorMarshall, "failed to marshall SARIF json file")
}
if err := utils.MkdirAll(ReportsDirectory, 0777); err != nil {
return reportPaths, errors.Wrapf(err, "failed to create report directory")
}
sarifReportPath := filepath.Join(ReportsDirectory, "piper_whitesource_vulnerability.sarif")
if err := utils.FileWrite(sarifReportPath, sarifReport, 0666); err != nil {
log.SetErrorCategory(log.ErrorConfiguration)
return reportPaths, errors.Wrapf(err, "failed to write SARIF file")
}
reportPaths = append(reportPaths, piperutils.Path{Name: "WhiteSource Vulnerability SARIF file", Target: sarifReportPath})
return reportPaths, nil
}