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 }