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

     1  package whitesource
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/sha1"
     6  	"encoding/base64"
     7  	"encoding/json"
     8  	"fmt"
     9  	"path/filepath"
    10  	"runtime"
    11  	"sort"
    12  	"strings"
    13  	"time"
    14  
    15  	cdx "github.com/CycloneDX/cyclonedx-go"
    16  	"github.com/package-url/packageurl-go"
    17  
    18  	"github.com/SAP/jenkins-library/pkg/format"
    19  	"github.com/SAP/jenkins-library/pkg/log"
    20  	"github.com/SAP/jenkins-library/pkg/piperutils"
    21  	"github.com/SAP/jenkins-library/pkg/reporting"
    22  	"github.com/pkg/errors"
    23  )
    24  
    25  // CreateCustomVulnerabilityReport creates a vulnerability ScanReport to be used for uploading into various sinks
    26  func CreateCustomVulnerabilityReport(productName string, scan *Scan, alerts *[]Alert, cvssSeverityLimit float64) reporting.ScanReport {
    27  	severe, _ := CountSecurityVulnerabilities(alerts, cvssSeverityLimit)
    28  
    29  	// sort according to vulnerability severity
    30  	sort.Slice(*alerts, func(i, j int) bool {
    31  		return vulnerabilityScore((*alerts)[i]) > vulnerabilityScore((*alerts)[j])
    32  	})
    33  
    34  	projectNames := scan.ScannedProjectNames()
    35  
    36  	scanReport := reporting.ScanReport{
    37  		ReportTitle: "WhiteSource Security Vulnerability Report",
    38  		Subheaders: []reporting.Subheader{
    39  			{Description: "WhiteSource product name", Details: productName},
    40  			{Description: "Filtered project names", Details: strings.Join(projectNames, ", ")},
    41  		},
    42  		Overview: []reporting.OverviewRow{
    43  			{Description: "Total number of vulnerabilities", Details: fmt.Sprint(len(*alerts))},
    44  			{Description: "Total number of high/critical vulnerabilities with CVSS score >= 7.0", Details: fmt.Sprint(severe)},
    45  		},
    46  		SuccessfulScan: severe == 0,
    47  		ReportTime:     time.Now(),
    48  	}
    49  
    50  	detailTable := reporting.ScanDetailTable{
    51  		NoRowsMessage: "No publicly known vulnerabilities detected",
    52  		Headers: []string{
    53  			"Date",
    54  			"CVE",
    55  			"CVSS Score",
    56  			"CVSS Version",
    57  			"Project",
    58  			"Library file name",
    59  			"Library group ID",
    60  			"Library artifact ID",
    61  			"Library version",
    62  			"Description",
    63  			"Top fix",
    64  		},
    65  		WithCounter:   true,
    66  		CounterHeader: "Entry #",
    67  	}
    68  
    69  	for _, alert := range *alerts {
    70  		var score float64
    71  		var scoreStyle reporting.ColumnStyle = reporting.Yellow
    72  		if isSevereVulnerability(alert, cvssSeverityLimit) {
    73  			scoreStyle = reporting.Red
    74  		}
    75  		var cveVersion string
    76  		if alert.Vulnerability.CVSS3Score > 0 {
    77  			score = alert.Vulnerability.CVSS3Score
    78  			cveVersion = "v3"
    79  		} else {
    80  			score = alert.Vulnerability.Score
    81  			cveVersion = "v2"
    82  		}
    83  
    84  		var topFix string
    85  		emptyFix := Fix{}
    86  		if alert.Vulnerability.TopFix != emptyFix {
    87  			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)
    88  		}
    89  
    90  		row := reporting.ScanRow{}
    91  		row.AddColumn(alert.Vulnerability.PublishDate, 0)
    92  		row.AddColumn(fmt.Sprintf(`<a href="%v">%v</a>`, alert.Vulnerability.URL, alert.Vulnerability.Name), 0)
    93  		row.AddColumn(score, scoreStyle)
    94  		row.AddColumn(cveVersion, 0)
    95  		row.AddColumn(alert.Project, 0)
    96  		row.AddColumn(alert.Library.Filename, 0)
    97  		row.AddColumn(alert.Library.GroupID, 0)
    98  		row.AddColumn(alert.Library.ArtifactID, 0)
    99  		row.AddColumn(alert.Library.Version, 0)
   100  		row.AddColumn(alert.Vulnerability.Description, 0)
   101  		row.AddColumn(topFix, 0)
   102  
   103  		detailTable.Rows = append(detailTable.Rows, row)
   104  	}
   105  	scanReport.DetailTable = detailTable
   106  
   107  	return scanReport
   108  }
   109  
   110  // CountSecurityVulnerabilities counts the security vulnerabilities above severityLimit
   111  func CountSecurityVulnerabilities(alerts *[]Alert, cvssSeverityLimit float64) (int, int) {
   112  	severeVulnerabilities := 0
   113  	for _, alert := range *alerts {
   114  		if isSevereVulnerability(alert, cvssSeverityLimit) {
   115  			severeVulnerabilities++
   116  		}
   117  	}
   118  
   119  	nonSevereVulnerabilities := len(*alerts) - severeVulnerabilities
   120  	return severeVulnerabilities, nonSevereVulnerabilities
   121  }
   122  
   123  func isSevereVulnerability(alert Alert, cvssSeverityLimit float64) bool {
   124  
   125  	if vulnerabilityScore(alert) >= cvssSeverityLimit && cvssSeverityLimit >= 0 {
   126  		return true
   127  	}
   128  	return false
   129  }
   130  
   131  func vulnerabilityScore(alert Alert) float64 {
   132  	if alert.Vulnerability.CVSS3Score > 0 {
   133  		return alert.Vulnerability.CVSS3Score
   134  	}
   135  	return alert.Vulnerability.Score
   136  }
   137  
   138  // ReportSha creates a SHA unique to the WS product and scan to be used as part of the report filename
   139  func ReportSha(productName string, scan *Scan) string {
   140  	reportShaData := []byte(productName + "," + strings.Join(scan.ScannedProjectNames(), ","))
   141  	return fmt.Sprintf("%x", sha1.Sum(reportShaData))
   142  }
   143  
   144  // WriteCustomVulnerabilityReports creates an HTML and a JSON format file based on the alerts brought up by the scan
   145  func WriteCustomVulnerabilityReports(productName string, scan *Scan, scanReport reporting.ScanReport, utils piperutils.FileUtils) ([]piperutils.Path, error) {
   146  	reportPaths := []piperutils.Path{}
   147  
   148  	// ignore templating errors since template is in our hands and issues will be detected with the automated tests
   149  	htmlReport, _ := scanReport.ToHTML()
   150  	if err := utils.MkdirAll(ReportsDirectory, 0777); err != nil {
   151  		return reportPaths, errors.Wrapf(err, "failed to create report directory")
   152  	}
   153  	htmlReportPath := filepath.Join(ReportsDirectory, "piper_whitesource_vulnerability_report.html")
   154  	if err := utils.FileWrite(htmlReportPath, htmlReport, 0666); err != nil {
   155  		log.SetErrorCategory(log.ErrorConfiguration)
   156  		return reportPaths, errors.Wrapf(err, "failed to write html report")
   157  	}
   158  	reportPaths = append(reportPaths, piperutils.Path{Name: "WhiteSource Vulnerability Report", Target: htmlReportPath})
   159  
   160  	// JSON reports are used by step pipelineCreateSummary in order to e.g. prepare an issue creation in GitHub
   161  	// ignore JSON errors since structure is in our hands
   162  	jsonReport, _ := scanReport.ToJSON()
   163  	if exists, _ := utils.DirExists(reporting.StepReportDirectory); !exists {
   164  		err := utils.MkdirAll(reporting.StepReportDirectory, 0777)
   165  		if err != nil {
   166  			return reportPaths, errors.Wrap(err, "failed to create step reporting directory")
   167  		}
   168  	}
   169  	if err := utils.FileWrite(filepath.Join(reporting.StepReportDirectory, fmt.Sprintf("whitesourceExecuteScan_oss_%v.json", ReportSha(productName, scan))), jsonReport, 0666); err != nil {
   170  		return reportPaths, errors.Wrapf(err, "failed to write json report")
   171  	}
   172  	// we do not add the json report to the overall list of reports for now,
   173  	// since it is just an intermediary report used as input for later
   174  	// and there does not seem to be real benefit in archiving it.
   175  
   176  	return reportPaths, nil
   177  }
   178  
   179  // Creates a SARIF result from the Alerts that were brought up by the scan
   180  func CreateSarifResultFile(scan *Scan, alerts *[]Alert) *format.SARIF {
   181  	//Now, we handle the sarif
   182  	log.Entry().Debug("Creating SARIF file for data transfer")
   183  	var sarif format.SARIF
   184  	sarif.Schema = "https://docs.oasis-open.org/sarif/sarif/v2.1.0/cos02/schemas/sarif-schema-2.1.0.json"
   185  	sarif.Version = "2.1.0"
   186  	var wsRun format.Runs
   187  	sarif.Runs = append(sarif.Runs, wsRun)
   188  
   189  	//handle the tool object
   190  	tool := *new(format.Tool)
   191  	tool.Driver = *new(format.Driver)
   192  	tool.Driver.Name = scan.AgentName
   193  	tool.Driver.Version = scan.AgentVersion
   194  	tool.Driver.InformationUri = "https://mend.io"
   195  
   196  	// Handle results/vulnerabilities
   197  	collectedRules := []string{}
   198  	for _, alert := range *alerts {
   199  		result := *new(format.Results)
   200  		ruleId := alert.Vulnerability.Name
   201  		log.Entry().Debugf("Transforming alert %v into SARIF format", ruleId)
   202  		result.RuleID = ruleId
   203  		result.Message = new(format.Message)
   204  		result.Message.Text = alert.Vulnerability.Description
   205  		artLoc := new(format.ArtifactLocation)
   206  		artLoc.Index = 0
   207  		artLoc.URI = alert.Library.Filename
   208  		result.AnalysisTarget = artLoc
   209  		location := format.Location{PhysicalLocation: format.PhysicalLocation{ArtifactLocation: format.ArtifactLocation{URI: alert.Library.Filename}}}
   210  		result.Locations = append(result.Locations, location)
   211  		partialFingerprints := new(format.PartialFingerprints)
   212  		partialFingerprints.PackageURLPlusCVEHash = base64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("%v+%v", alert.Library.ToPackageUrl().ToString(), alert.Vulnerability.Name)))
   213  		result.PartialFingerprints = *partialFingerprints
   214  		result.Properties = getAuditInformation(alert)
   215  
   216  		//append the result
   217  		sarif.Runs[0].Results = append(sarif.Runs[0].Results, result)
   218  
   219  		// only create rule on new CVE
   220  		if !piperutils.ContainsString(collectedRules, ruleId) {
   221  			collectedRules = append(collectedRules, ruleId)
   222  
   223  			sarifRule := *new(format.SarifRule)
   224  			sarifRule.ID = ruleId
   225  			sarifRule.Name = alert.Vulnerability.Name
   226  			sd := new(format.Message)
   227  			sd.Text = fmt.Sprintf("%v Package %v", alert.Vulnerability.Name, alert.Library.ArtifactID)
   228  			sarifRule.ShortDescription = sd
   229  			fd := new(format.Message)
   230  			fd.Text = alert.Vulnerability.Description
   231  			sarifRule.FullDescription = fd
   232  			defaultConfig := new(format.DefaultConfiguration)
   233  			defaultConfig.Level = transformToLevel(alert.Vulnerability.Severity, alert.Vulnerability.CVSS3Severity)
   234  			sarifRule.DefaultConfiguration = defaultConfig
   235  			sarifRule.HelpURI = alert.Vulnerability.URL
   236  			markdown, _ := alert.ToMarkdown()
   237  			sarifRule.Help = new(format.Help)
   238  			sarifRule.Help.Text = alert.ToTxt()
   239  			sarifRule.Help.Markdown = string(markdown)
   240  
   241  			ruleProp := *new(format.SarifRuleProperties)
   242  			ruleProp.Tags = append(ruleProp.Tags, alert.Type)
   243  			ruleProp.Tags = append(ruleProp.Tags, alert.Library.ToPackageUrl().ToString())
   244  			ruleProp.Tags = append(ruleProp.Tags, alert.Vulnerability.URL)
   245  			ruleProp.SecuritySeverity = fmt.Sprint(consolidateScores(alert.Vulnerability.Score, alert.Vulnerability.CVSS3Score))
   246  			ruleProp.Precision = "very-high"
   247  
   248  			sarifRule.Properties = &ruleProp
   249  
   250  			// append the rule
   251  			tool.Driver.Rules = append(tool.Driver.Rules, sarifRule)
   252  		}
   253  	}
   254  	//Finalize: tool
   255  	sarif.Runs[0].Tool = tool
   256  
   257  	// Threadflowlocations is no loger useful: voiding it will make for smaller reports
   258  	sarif.Runs[0].ThreadFlowLocations = []format.Locations{}
   259  
   260  	// Add a conversion object to highlight this isn't native SARIF
   261  	conversion := new(format.Conversion)
   262  	conversion.Tool.Driver.Name = "Piper FPR to SARIF converter"
   263  	conversion.Tool.Driver.InformationUri = "https://github.com/SAP/jenkins-library"
   264  	conversion.Invocation.ExecutionSuccessful = true
   265  	convInvocProp := new(format.InvocationProperties)
   266  	convInvocProp.Platform = runtime.GOOS
   267  	conversion.Invocation.Properties = convInvocProp
   268  	sarif.Runs[0].Conversion = conversion
   269  
   270  	return &sarif
   271  }
   272  
   273  func getAuditInformation(alert Alert) *format.SarifProperties {
   274  	unifiedAuditState := "new"
   275  	auditMessage := ""
   276  	isAudited := false
   277  
   278  	// unified audit state
   279  	switch alert.Status {
   280  	case "OPEN":
   281  		unifiedAuditState = "new"
   282  	case "IGNORE":
   283  		unifiedAuditState = "notRelevant"
   284  		auditMessage = alert.Comments
   285  	}
   286  
   287  	if alert.Assessment != nil {
   288  		unifiedAuditState = string(alert.Assessment.Status)
   289  		auditMessage = string(alert.Assessment.Analysis)
   290  	}
   291  
   292  	if unifiedAuditState == string(format.Relevant) ||
   293  		unifiedAuditState == string(format.NotRelevant) {
   294  		isAudited = true
   295  	}
   296  
   297  	return &format.SarifProperties{
   298  		Audited:               isAudited,
   299  		ToolAuditMessage:      auditMessage,
   300  		UnifiedAuditState:     unifiedAuditState,
   301  		AuditRequirement:      format.AUDIT_REQUIREMENT_GROUP_1_DESC,
   302  		AuditRequirementIndex: format.AUDIT_REQUIREMENT_GROUP_1_INDEX,
   303  		UnifiedSeverity:       alert.Vulnerability.CVSS3Severity,
   304  		UnifiedCriticality:    float32(alert.Vulnerability.CVSS3Score),
   305  	}
   306  }
   307  
   308  func transformToLevel(cvss2severity, cvss3severity string) string {
   309  	cvssseverity := consolidateSeverities(cvss2severity, cvss3severity)
   310  	switch cvssseverity {
   311  	case "low":
   312  		return "warning"
   313  	case "medium":
   314  		return "warning"
   315  	case "high":
   316  		return "error"
   317  	case "critical":
   318  		return "error"
   319  	}
   320  	return "none"
   321  }
   322  
   323  func consolidateSeverities(cvss2severity, cvss3severity string) string {
   324  	if len(cvss3severity) > 0 {
   325  		return cvss3severity
   326  	}
   327  	return cvss2severity
   328  }
   329  
   330  // WriteSarifFile write a JSON sarif format file for upload into e.g. GCP
   331  func WriteSarifFile(sarif *format.SARIF, utils piperutils.FileUtils) ([]piperutils.Path, error) {
   332  	reportPaths := []piperutils.Path{}
   333  
   334  	// ignore templating errors since template is in our hands and issues will be detected with the automated tests
   335  	sarifReport, errorMarshall := json.Marshal(sarif)
   336  	if errorMarshall != nil {
   337  		return reportPaths, errors.Wrapf(errorMarshall, "failed to marshall SARIF json file")
   338  	}
   339  	if err := utils.MkdirAll(ReportsDirectory, 0777); err != nil {
   340  		return reportPaths, errors.Wrapf(err, "failed to create report directory")
   341  	}
   342  	sarifReportPath := filepath.Join(ReportsDirectory, "piper_whitesource_vulnerability.sarif")
   343  	if err := utils.FileWrite(sarifReportPath, sarifReport, 0666); err != nil {
   344  		log.SetErrorCategory(log.ErrorConfiguration)
   345  		return reportPaths, errors.Wrapf(err, "failed to write SARIF file")
   346  	}
   347  	reportPaths = append(reportPaths, piperutils.Path{Name: "WhiteSource Vulnerability SARIF file", Target: sarifReportPath})
   348  
   349  	return reportPaths, nil
   350  }
   351  
   352  func transformToCdxSeverity(severity string) cdx.Severity {
   353  	switch severity {
   354  	case "info":
   355  		return cdx.SeverityInfo
   356  	case "low":
   357  		return cdx.SeverityLow
   358  	case "medium":
   359  		return cdx.SeverityMedium
   360  	case "high":
   361  		return cdx.SeverityHigh
   362  	case "critical":
   363  		return cdx.SeverityCritical
   364  	case "":
   365  		return cdx.SeverityNone
   366  	}
   367  	return cdx.SeverityUnknown
   368  }
   369  
   370  func transformBuildToPurlType(buildType string) string {
   371  	switch buildType {
   372  	case "maven":
   373  		return packageurl.TypeMaven
   374  	case "npm":
   375  		return packageurl.TypeNPM
   376  	case "docker":
   377  		return packageurl.TypeDocker
   378  	case "kaniko":
   379  		return packageurl.TypeDocker
   380  	case "golang":
   381  		return packageurl.TypeGolang
   382  	case "mta":
   383  		return packageurl.TypeComposer
   384  	}
   385  	return packageurl.TypeGeneric
   386  }
   387  
   388  func CreateCycloneSBOM(scan *Scan, libraries *[]Library, alerts, assessedAlerts *[]Alert) ([]byte, error) {
   389  	ppurl := packageurl.NewPackageURL(transformBuildToPurlType(scan.BuildTool), scan.Coordinates.GroupID, scan.Coordinates.ArtifactID, scan.Coordinates.Version, nil, "")
   390  	metadata := cdx.Metadata{
   391  		// Define metadata about the main component
   392  		// (the component which the BOM will describe)
   393  
   394  		// TODO check whether we can identify library vs. application
   395  		Component: &cdx.Component{
   396  			BOMRef:     ppurl.ToString(),
   397  			Type:       cdx.ComponentTypeLibrary,
   398  			Name:       scan.Coordinates.ArtifactID,
   399  			Group:      scan.Coordinates.GroupID,
   400  			Version:    scan.Coordinates.Version,
   401  			PackageURL: ppurl.ToString(),
   402  		},
   403  		// Use properties to include an internal identifier for this BOM
   404  		// https://cyclonedx.org/use-cases/#properties--name-value-store
   405  		Properties: &[]cdx.Property{
   406  			{
   407  				Name:  "internal:ws-product-identifier",
   408  				Value: scan.ProductToken,
   409  			},
   410  			{
   411  				Name:  "internal:ws-project-identifier",
   412  				Value: strings.Join(scan.ScannedProjectTokens(), ", "),
   413  			},
   414  		},
   415  	}
   416  
   417  	components := []cdx.Component{}
   418  	flatUniqueLibrariesMap := map[string]Library{}
   419  	transformToUniqueFlatList(libraries, &flatUniqueLibrariesMap, 1)
   420  	flatUniqueLibraries := piperutils.Values(flatUniqueLibrariesMap)
   421  	log.Entry().Debugf("Got %v unique libraries in condensed flat list", len(flatUniqueLibraries))
   422  	sort.Slice(flatUniqueLibraries, func(i, j int) bool {
   423  		return flatUniqueLibraries[i].ToPackageUrl().ToString() < flatUniqueLibraries[j].ToPackageUrl().ToString()
   424  	})
   425  	for _, lib := range flatUniqueLibraries {
   426  		purl := lib.ToPackageUrl()
   427  		// Define the components that the product ships with
   428  		// https://cyclonedx.org/use-cases/#inventory
   429  		component := cdx.Component{
   430  			BOMRef:     purl.ToString(),
   431  			Type:       cdx.ComponentTypeLibrary,
   432  			Author:     lib.GroupID,
   433  			Name:       lib.ArtifactID,
   434  			Version:    lib.Version,
   435  			PackageURL: purl.ToString(),
   436  			Hashes:     &[]cdx.Hash{{Algorithm: cdx.HashAlgoSHA1, Value: lib.Sha1}},
   437  		}
   438  		components = append(components, component)
   439  	}
   440  
   441  	dependencies := []cdx.Dependency{}
   442  	declareDependency(ppurl, libraries, &dependencies)
   443  
   444  	// Encode vulnerabilities
   445  	vulnerabilities := []cdx.Vulnerability{}
   446  	vulnerabilities = append(vulnerabilities, transformAlertsToVulnerabilities(scan, alerts)...)
   447  	vulnerabilities = append(vulnerabilities, transformAlertsToVulnerabilities(scan, assessedAlerts)...)
   448  
   449  	// Assemble the BOM
   450  	bom := cdx.NewBOM()
   451  	bom.Vulnerabilities = &vulnerabilities
   452  	bom.Metadata = &metadata
   453  	bom.Components = &components
   454  	bom.Dependencies = &dependencies
   455  
   456  	// Encode the BOM
   457  	var outputBytes []byte
   458  	buffer := bytes.NewBuffer(outputBytes)
   459  	encoder := cdx.NewBOMEncoder(buffer, cdx.BOMFileFormatXML)
   460  	encoder.SetPretty(true)
   461  	if err := encoder.Encode(bom); err != nil {
   462  		return nil, err
   463  	}
   464  	return buffer.Bytes(), nil
   465  }
   466  
   467  func transformAlertsToVulnerabilities(scan *Scan, alerts *[]Alert) []cdx.Vulnerability {
   468  	vulnerabilities := []cdx.Vulnerability{}
   469  	for _, alert := range *alerts {
   470  		// Define the vulnerabilities in VEX
   471  		// https://cyclonedx.org/use-cases/#vulnerability-exploitability
   472  		purl := alert.Library.ToPackageUrl()
   473  		advisories := []cdx.Advisory{}
   474  		for _, fix := range alert.Vulnerability.AllFixes {
   475  			advisory := cdx.Advisory{
   476  				Title: fix.Message,
   477  				URL:   alert.Vulnerability.TopFix.URL,
   478  			}
   479  			advisories = append(advisories, advisory)
   480  		}
   481  		cvss3Score := alert.Vulnerability.CVSS3Score
   482  		cvssScore := alert.Vulnerability.Score
   483  		vuln := cdx.Vulnerability{
   484  			BOMRef: purl.ToString(),
   485  			ID:     alert.Vulnerability.Name,
   486  			Source: &cdx.Source{URL: alert.Vulnerability.URL},
   487  			Tools: &[]cdx.Tool{
   488  				{
   489  					Name:    scan.AgentName,
   490  					Version: scan.AgentVersion,
   491  					Vendor:  "Mend",
   492  					ExternalReferences: &[]cdx.ExternalReference{
   493  						{
   494  							URL:  "https://www.mend.io/",
   495  							Type: cdx.ERTypeBuildMeta,
   496  						},
   497  					},
   498  				},
   499  			},
   500  			Recommendation: alert.Vulnerability.FixResolutionText,
   501  			Detail:         alert.Vulnerability.URL,
   502  			Ratings: &[]cdx.VulnerabilityRating{
   503  				{
   504  					Score:    &cvss3Score,
   505  					Severity: transformToCdxSeverity(alert.Vulnerability.CVSS3Severity),
   506  					Method:   cdx.ScoringMethodCVSSv3,
   507  				},
   508  				{
   509  					Score:    &cvssScore,
   510  					Severity: transformToCdxSeverity(alert.Vulnerability.Severity),
   511  					Method:   cdx.ScoringMethodCVSSv2,
   512  				},
   513  			},
   514  			Advisories:  &advisories,
   515  			Description: alert.Vulnerability.Description,
   516  			Created:     alert.CreationDate,
   517  			Published:   alert.Vulnerability.PublishDate,
   518  			Updated:     alert.ModifiedDate,
   519  			Affects: &[]cdx.Affects{
   520  				{
   521  					Ref: purl.ToString(),
   522  					Range: &[]cdx.AffectedVersions{
   523  						{
   524  							Version: alert.Library.Version,
   525  							Status:  cdx.VulnerabilityStatus(alert.Status),
   526  						},
   527  					},
   528  				},
   529  			},
   530  		}
   531  		references := []cdx.VulnerabilityReference{}
   532  		for _, ref := range alert.Vulnerability.References {
   533  			reference := cdx.VulnerabilityReference{
   534  				Source: &cdx.Source{Name: ref.Homepage, URL: ref.URL},
   535  				ID:     ref.GenericPackageIndex,
   536  			}
   537  			references = append(references, reference)
   538  		}
   539  		vuln.References = &references
   540  		if alert.Assessment != nil {
   541  			vuln.Analysis = &cdx.VulnerabilityAnalysis{
   542  				State:         alert.Assessment.ToImpactAnalysisState(),
   543  				Justification: alert.Assessment.ToImpactJustification(),
   544  				Response:      alert.Assessment.ToImpactAnalysisResponse(),
   545  			}
   546  		}
   547  
   548  		vulnerabilities = append(vulnerabilities, vuln)
   549  	}
   550  	return vulnerabilities
   551  }
   552  
   553  func WriteCycloneSBOM(sbom []byte, utils piperutils.FileUtils) ([]piperutils.Path, error) {
   554  	paths := []piperutils.Path{}
   555  	if err := utils.MkdirAll(ReportsDirectory, 0777); err != nil {
   556  		return paths, errors.Wrapf(err, "failed to create report directory")
   557  	}
   558  
   559  	sbomPath := filepath.Join(ReportsDirectory, "piper_whitesource_sbom.xml")
   560  
   561  	// Write file
   562  	if err := utils.FileWrite(sbomPath, sbom, 0666); err != nil {
   563  		log.SetErrorCategory(log.ErrorConfiguration)
   564  		return paths, errors.Wrapf(err, "failed to write SARIF file")
   565  	}
   566  	paths = append(paths, piperutils.Path{Name: "WhiteSource SBOM file", Target: sbomPath})
   567  
   568  	return paths, nil
   569  }
   570  
   571  func transformToUniqueFlatList(libraries *[]Library, flatMapRef *map[string]Library, level int) {
   572  	log.Entry().Debugf("Got %v libraries reported on level %v", len(*libraries), level)
   573  	for _, lib := range *libraries {
   574  		key := lib.ToPackageUrl().ToString()
   575  		flatMap := *flatMapRef
   576  		lookup := flatMap[key]
   577  		if lookup.KeyID != lib.KeyID {
   578  			flatMap[key] = lib
   579  			if len(lib.Dependencies) > 0 {
   580  				transformToUniqueFlatList(&lib.Dependencies, flatMapRef, level+1)
   581  			}
   582  
   583  		}
   584  	}
   585  }
   586  
   587  func declareDependency(parentPurl *packageurl.PackageURL, dependents *[]Library, collection *[]cdx.Dependency) {
   588  	localDependencies := []cdx.Dependency{}
   589  	for _, lib := range *dependents {
   590  		purl := lib.ToPackageUrl()
   591  		// Define the dependency graph
   592  		// https://cyclonedx.org/use-cases/#dependency-graph
   593  		localDependency := cdx.Dependency{Ref: purl.ToString()}
   594  		localDependencies = append(localDependencies, localDependency)
   595  
   596  		if len(lib.Dependencies) > 0 {
   597  			declareDependency(purl, &lib.Dependencies, collection)
   598  		}
   599  	}
   600  	dependency := cdx.Dependency{
   601  		Ref:          parentPurl.ToString(),
   602  		Dependencies: &localDependencies,
   603  	}
   604  	*collection = append(*collection, dependency)
   605  }