github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/pkg/whitesource/whitesource.go (about) 1 package whitesource 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/http" 9 "strings" 10 "time" 11 12 "github.com/SAP/jenkins-library/pkg/format" 13 piperhttp "github.com/SAP/jenkins-library/pkg/http" 14 "github.com/SAP/jenkins-library/pkg/log" 15 "github.com/SAP/jenkins-library/pkg/reporting" 16 "github.com/package-url/packageurl-go" 17 "github.com/pkg/errors" 18 ) 19 20 // ReportsDirectory defines the subfolder for the WhiteSource reports which are generated 21 const ReportsDirectory = "whitesource" 22 23 // Product defines a WhiteSource product with name and token 24 type Product struct { 25 Name string `json:"name"` 26 Token string `json:"token"` 27 CreationDate string `json:"creationDate,omitempty"` 28 LastUpdateDate string `json:"lastUpdatedDate,omitempty"` 29 } 30 31 // Assignment describes a list of UserAssignments and GroupAssignments which can be attributed to a WhiteSource Product. 32 type Assignment struct { 33 UserAssignments []UserAssignment `json:"userAssignments,omitempty"` 34 GroupAssignments []GroupAssignment `json:"groupAssignments,omitempty"` 35 } 36 37 // UserAssignment holds an email address for a WhiteSource user 38 // which can be assigned to a WhiteSource Product in a specific role. 39 type UserAssignment struct { 40 Email string `json:"email,omitempty"` 41 } 42 43 // GroupAssignment refers to the name of a particular group in WhiteSource. 44 type GroupAssignment struct { 45 Name string `json:"name,omitempty"` 46 } 47 48 // Alert 49 type Alert struct { 50 *format.Assessment 51 Vulnerability Vulnerability `json:"vulnerability"` 52 Type string `json:"type,omitempty"` 53 Level string `json:"level,omitempty"` 54 Library Library `json:"library,omitempty"` 55 Project string `json:"project,omitempty"` 56 DirectDependency bool `json:"directDependency,omitempty"` 57 Description string `json:"description,omitempty"` 58 CreationDate string `json:"date,omitempty"` 59 ModifiedDate string `json:"modifiedDate,omitempty"` 60 Status string `json:"status,omitempty"` 61 Comments string `json:"comments,omitempty"` 62 } 63 64 // DependencyType returns type of dependency: direct/transitive 65 func (a *Alert) DependencyType() string { 66 if a.DirectDependency == true { 67 return "direct" 68 } 69 return "transitive" 70 } 71 72 // Title returns the issue title representation of the contents 73 func (a Alert) Title() string { 74 if a.Type == "SECURITY_VULNERABILITY" { 75 return fmt.Sprintf("Security Vulnerability %v %v", a.Vulnerability.Name, a.Library.ArtifactID) 76 } else if a.Type == "REJECTED_BY_POLICY_RESOURCE" { 77 return fmt.Sprintf("Policy Violation %v %v", a.Vulnerability.Name, a.Library.ArtifactID) 78 } 79 return fmt.Sprintf("%v %v %v ", a.Type, a.Vulnerability.Name, a.Library.ArtifactID) 80 } 81 82 func (a *Alert) ContainedIn(assessments *[]format.Assessment) (bool, error) { 83 localPurl := a.Library.ToPackageUrl().ToString() 84 for _, assessment := range *assessments { 85 if assessment.Vulnerability == a.Vulnerability.Name { 86 for _, purl := range assessment.Purls { 87 assessmentPurl, err := purl.ToPackageUrl() 88 assessmentPurlStr := assessmentPurl.ToString() 89 if err != nil { 90 log.SetErrorCategory(log.ErrorConfiguration) 91 log.Entry().WithError(err).Errorf("assessment from file ignored due to invalid packageUrl '%s'", purl) 92 return false, err 93 } 94 if assessmentPurlStr == localPurl { 95 log.Entry().Debugf("matching assessment %v on package %v detected for alert %v", assessment.Vulnerability, assessmentPurlStr, a.Vulnerability.Name) 96 a.Assessment = &assessment 97 return true, nil 98 } 99 } 100 } 101 } 102 return false, nil 103 } 104 105 func transformLibToPurlType(libType string) string { 106 log.Entry().Debugf("LibType reported as %v", libType) 107 switch strings.ToLower(libType) { 108 case "java": 109 fallthrough 110 case "maven_artifact": 111 return packageurl.TypeMaven 112 case "javascript/node.js": 113 fallthrough 114 case "node_packaged_module": 115 return packageurl.TypeNPM 116 case "javascript/bower": 117 return "bower" 118 case "go": 119 fallthrough 120 case "go_package": 121 return packageurl.TypeGolang 122 case "python": 123 fallthrough 124 case "python_package": 125 return packageurl.TypePyPi 126 case "debian": 127 fallthrough 128 case "debian_package": 129 return packageurl.TypeDebian 130 case "docker": 131 return packageurl.TypeDocker 132 case ".net": 133 fallthrough 134 case "dot_net_resource": 135 return packageurl.TypeNuget 136 } 137 return packageurl.TypeGeneric 138 } 139 140 func consolidate(cvss2severity, cvss3severity string, cvss2score, cvss3score float64) string { 141 cvssseverity := consolidateSeverities(cvss2severity, cvss3severity) 142 switch cvssseverity { 143 case "low": 144 return "LOW" 145 case "medium": 146 return "MEDIUM" 147 case "high": 148 if cvss3score >= 9 || cvss2score >= 9 { 149 return "CRITICAL" 150 } 151 return "HIGH" 152 } 153 return "none" 154 } 155 156 // ToMarkdown returns the markdown representation of the contents 157 func (a Alert) ToMarkdown() ([]byte, error) { 158 159 if a.Type == "SECURITY_VULNERABILITY" { 160 score := consolidateScores(a.Vulnerability.Score, a.Vulnerability.CVSS3Score) 161 162 vul := reporting.VulnerabilityReport{ 163 ArtifactID: a.Library.ArtifactID, 164 // no information available about branch and commit, yet 165 Branch: "", 166 CommitID: "", 167 Description: a.Vulnerability.Description, 168 DependencyType: a.DependencyType(), 169 // no information available about footer, yet 170 Footer: "", 171 Group: a.Library.GroupID, 172 // no information available about pipeline name and link, yet 173 PipelineName: "", 174 PipelineLink: "", 175 PublishDate: a.Vulnerability.PublishDate, 176 Resolution: a.Vulnerability.TopFix.FixResolution, 177 Score: score, 178 Severity: consolidate(a.Vulnerability.Severity, a.Vulnerability.CVSS3Severity, a.Vulnerability.Score, a.Vulnerability.CVSS3Score), 179 Version: a.Library.Version, 180 PackageURL: a.Library.ToPackageUrl().ToString(), 181 VulnerabilityLink: a.Vulnerability.URL, 182 VulnerabilityName: a.Vulnerability.Name, 183 } 184 return vul.ToMarkdown() 185 } else if a.Type == "REJECTED_BY_POLICY_RESOURCE" { 186 policyReport := reporting.PolicyViolationReport{ 187 ArtifactID: a.Library.ArtifactID, 188 // no information available about branch and commit, yet 189 Branch: "", 190 CommitID: "", 191 Description: a.Vulnerability.Description, 192 DirectDependency: fmt.Sprint(a.DirectDependency), 193 // no information available about footer, yet 194 Footer: "", 195 Group: a.Library.GroupID, 196 // no information available about pipeline name and link, yet 197 PipelineName: "", 198 PipelineLink: "", 199 Version: a.Library.Version, 200 PackageURL: a.Library.ToPackageUrl().ToString(), 201 } 202 return policyReport.ToMarkdown() 203 } 204 205 return []byte{}, nil 206 } 207 208 // ToTxt returns the textual representation of the contents 209 func (a Alert) ToTxt() string { 210 score := consolidateScores(a.Vulnerability.Score, a.Vulnerability.CVSS3Score) 211 return fmt.Sprintf(`Vulnerability %v 212 Severity: %v 213 Base (NVD) Score: %v 214 Package: %v 215 Installed Version: %v 216 Package URL: %v 217 Description: %v 218 Fix Resolution: %v 219 Link: [%v](%v)`, 220 a.Vulnerability.Name, 221 a.Vulnerability.Severity, 222 score, 223 a.Library.ArtifactID, 224 a.Library.Version, 225 a.Library.ToPackageUrl().ToString(), 226 a.Vulnerability.Description, 227 a.Vulnerability.TopFix.FixResolution, 228 a.Vulnerability.Name, 229 a.Vulnerability.URL, 230 ) 231 } 232 233 func consolidateScores(cvss2score, cvss3score float64) float64 { 234 score := cvss3score 235 if score == 0 { 236 score = cvss2score 237 } 238 return score 239 } 240 241 // Library 242 type Library struct { 243 KeyUUID string `json:"keyUuid,omitempty"` 244 KeyID int `json:"keyId,omitempty"` 245 Name string `json:"name,omitempty"` 246 Filename string `json:"filename,omitempty"` 247 ArtifactID string `json:"artifactId,omitempty"` 248 GroupID string `json:"groupId,omitempty"` 249 Version string `json:"version,omitempty"` 250 Sha1 string `json:"sha1,omitempty"` 251 LibType string `json:"type,omitempty"` 252 Coordinates string `json:"coordinates,omitempty"` 253 Dependencies []Library `json:"dependencies,omitempty"` 254 } 255 256 // ToPackageUrl constructs and returns the package URL of the library 257 func (l Library) ToPackageUrl() *packageurl.PackageURL { 258 return packageurl.NewPackageURL(transformLibToPurlType(l.LibType), l.GroupID, l.ArtifactID, l.Version, nil, "") 259 } 260 261 // Vulnerability defines a vulnerability as returned by WhiteSource 262 type Vulnerability struct { 263 Name string `json:"name,omitempty"` 264 Type string `json:"type,omitempty"` 265 Severity string `json:"severity,omitempty"` 266 Score float64 `json:"score,omitempty"` 267 CVSS3Severity string `json:"cvss3_severity,omitempty"` 268 CVSS3Score float64 `json:"cvss3_score,omitempty"` 269 PublishDate string `json:"publishDate,omitempty"` 270 URL string `json:"url,omitempty"` 271 Description string `json:"description,omitempty"` 272 TopFix Fix `json:"topFix,omitempty"` 273 AllFixes []Fix `json:"allFixes,omitempty"` 274 FixResolutionText string `json:"fixResolutionText,omitempty"` 275 References []Reference `json:"references,omitempty"` 276 } 277 278 // Fix defines a Fix as returned by WhiteSource 279 type Fix struct { 280 Vulnerability string `json:"vulnerability,omitempty"` 281 Type string `json:"type,omitempty"` 282 Origin string `json:"origin,omitempty"` 283 URL string `json:"url,omitempty"` 284 FixResolution string `json:"fixResolution,omitempty"` 285 Date string `json:"date,omitempty"` 286 Message string `json:"message,omitempty"` 287 ExtraData string `json:"extraData,omitempty"` 288 } 289 290 // Reference defines a reference for the library affected 291 type Reference struct { 292 URL string `json:"url,omitempty"` 293 Homepage string `json:"homepage,omitempty"` 294 GenericPackageIndex string `json:"genericPackageIndex,omitempty"` 295 } 296 297 // Project defines a WhiteSource project with name and token 298 type Project struct { 299 ID int64 `json:"id"` 300 Name string `json:"name"` 301 PluginName string `json:"pluginName"` 302 Token string `json:"token"` 303 UploadedBy string `json:"uploadedBy"` 304 CreationDate string `json:"creationDate,omitempty"` 305 LastUpdateDate string `json:"lastUpdatedDate,omitempty"` 306 } 307 308 // Request defines a request object to be sent to the WhiteSource system 309 type Request struct { 310 RequestType string `json:"requestType,omitempty"` 311 UserKey string `json:"userKey,omitempty"` 312 ProductToken string `json:"productToken,omitempty"` 313 ProductName string `json:"productName,omitempty"` 314 ProjectToken string `json:"projectToken,omitempty"` 315 OrgToken string `json:"orgToken,omitempty"` 316 Format string `json:"format,omitempty"` 317 AlertType string `json:"alertType,omitempty"` 318 ProductAdmins *Assignment `json:"productAdmins,omitempty"` 319 ProductMembership *Assignment `json:"productMembership,omitempty"` 320 AlertsEmailReceivers *Assignment `json:"alertsEmailReceivers,omitempty"` 321 ProductApprovers *Assignment `json:"productApprovers,omitempty"` 322 ProductIntegrators *Assignment `json:"productIntegrators,omitempty"` 323 IncludeInHouseData bool `json:"includeInHouseData,omitempty"` 324 } 325 326 // System defines a WhiteSource System including respective tokens (e.g. org token, user token) 327 type System struct { 328 httpClient piperhttp.Sender 329 orgToken string 330 serverURL string 331 userToken string 332 maxRetries int 333 retryInterval time.Duration 334 } 335 336 // DateTimeLayout is the layout of the time format used by the WhiteSource API. 337 const DateTimeLayout = "2006-01-02 15:04:05 -0700" 338 339 // NewSystem constructs a new System instance 340 func NewSystem(serverURL, orgToken, userToken string, timeout time.Duration) *System { 341 httpClient := &piperhttp.Client{} 342 httpClient.SetOptions(piperhttp.ClientOptions{TransportTimeout: timeout}) 343 return &System{ 344 serverURL: serverURL, 345 orgToken: orgToken, 346 userToken: userToken, 347 httpClient: httpClient, 348 maxRetries: 10, 349 retryInterval: 3 * time.Second, 350 } 351 } 352 353 // GetProductsMetaInfo retrieves meta information for all WhiteSource products a user has access to 354 func (s *System) GetProductsMetaInfo() ([]Product, error) { 355 wsResponse := struct { 356 ProductVitals []Product `json:"productVitals"` 357 }{ 358 ProductVitals: []Product{}, 359 } 360 361 req := Request{ 362 RequestType: "getOrganizationProductVitals", 363 } 364 365 err := s.sendRequestAndDecodeJSON(req, &wsResponse) 366 if err != nil { 367 return wsResponse.ProductVitals, err 368 } 369 370 return wsResponse.ProductVitals, nil 371 } 372 373 // GetProductByName retrieves meta information for a specific WhiteSource product 374 func (s *System) GetProductByName(productName string) (Product, error) { 375 products, err := s.GetProductsMetaInfo() 376 if err != nil { 377 return Product{}, errors.Wrap(err, "failed to retrieve WhiteSource products") 378 } 379 380 for _, p := range products { 381 if p.Name == productName { 382 return p, nil 383 } 384 } 385 386 return Product{}, fmt.Errorf("product '%v' not found in WhiteSource", productName) 387 } 388 389 // CreateProduct creates a new WhiteSource product and returns its product token. 390 func (s *System) CreateProduct(productName string) (string, error) { 391 wsResponse := struct { 392 ProductToken string `json:"productToken"` 393 }{ 394 ProductToken: "", 395 } 396 397 req := Request{ 398 RequestType: "createProduct", 399 ProductName: productName, 400 } 401 402 err := s.sendRequestAndDecodeJSON(req, &wsResponse) 403 if err != nil { 404 return "", err 405 } 406 407 return wsResponse.ProductToken, nil 408 } 409 410 // SetProductAssignments assigns various types of membership to a WhiteSource Product. 411 func (s *System) SetProductAssignments(productToken string, membership, admins, alertReceivers *Assignment) error { 412 req := Request{ 413 RequestType: "setProductAssignments", 414 ProductToken: productToken, 415 ProductMembership: membership, 416 ProductAdmins: admins, 417 AlertsEmailReceivers: alertReceivers, 418 } 419 420 err := s.sendRequestAndDecodeJSON(req, nil) 421 if err != nil { 422 return err 423 } 424 425 return nil 426 } 427 428 // GetProjectsMetaInfo retrieves the registered projects for a specific WhiteSource product 429 func (s *System) GetProjectsMetaInfo(productToken string) ([]Project, error) { 430 wsResponse := struct { 431 ProjectVitals []Project `json:"projectVitals"` 432 }{ 433 ProjectVitals: []Project{}, 434 } 435 436 req := Request{ 437 RequestType: "getProductProjectVitals", 438 ProductToken: productToken, 439 } 440 441 err := s.sendRequestAndDecodeJSON(req, &wsResponse) 442 if err != nil { 443 return nil, err 444 } 445 446 return wsResponse.ProjectVitals, nil 447 } 448 449 // GetProjectHierarchy retrieves the full set of libraries that the project depends on 450 func (s *System) GetProjectHierarchy(projectToken string, includeInHouse bool) ([]Library, error) { 451 wsResponse := struct { 452 Libraries []Library `json:"libraries"` 453 }{ 454 Libraries: []Library{}, 455 } 456 457 req := Request{ 458 RequestType: "getProjectHierarchy", 459 ProjectToken: projectToken, 460 IncludeInHouseData: includeInHouse, 461 } 462 463 err := s.sendRequestAndDecodeJSON(req, &wsResponse) 464 if err != nil { 465 return nil, err 466 } 467 468 return wsResponse.Libraries, nil 469 } 470 471 // GetProjectToken returns the project token for a project with a given name 472 func (s *System) GetProjectToken(productToken, projectName string) (string, error) { 473 project, err := s.GetProjectByName(productToken, projectName) 474 if err != nil { 475 return "", err 476 } 477 return project.Token, nil 478 } 479 480 // GetProjectByToken returns project meta info given a project token 481 func (s *System) GetProjectByToken(projectToken string) (Project, error) { 482 wsResponse := struct { 483 ProjectVitals []Project `json:"projectVitals"` 484 }{ 485 ProjectVitals: []Project{}, 486 } 487 488 req := Request{ 489 RequestType: "getProjectVitals", 490 ProjectToken: projectToken, 491 } 492 493 err := s.sendRequestAndDecodeJSON(req, &wsResponse) 494 if err != nil { 495 return Project{}, err 496 } 497 498 if len(wsResponse.ProjectVitals) == 0 { 499 return Project{}, errors.Wrapf(err, "no project with token '%s' found in WhiteSource", projectToken) 500 } 501 502 return wsResponse.ProjectVitals[0], nil 503 } 504 505 // GetProjectByName fetches all projects and returns the one matching the given projectName, or none, if not found 506 func (s *System) GetProjectByName(productToken, projectName string) (Project, error) { 507 projects, err := s.GetProjectsMetaInfo(productToken) 508 if err != nil { 509 return Project{}, errors.Wrap(err, "failed to retrieve WhiteSource project meta info") 510 } 511 512 for _, project := range projects { 513 if projectName == project.Name { 514 return project, nil 515 } 516 } 517 518 // returns empty project and no error. The reason seems to be that it makes polling until the project exists easier. 519 return Project{}, nil 520 } 521 522 // GetProjectsByIDs retrieves all projects for the given productToken and filters them by the given project ids 523 func (s *System) GetProjectsByIDs(productToken string, projectIDs []int64) ([]Project, error) { 524 projects, err := s.GetProjectsMetaInfo(productToken) 525 if err != nil { 526 return nil, errors.Wrap(err, "failed to retrieve WhiteSource project meta info") 527 } 528 529 var projectsMatched []Project 530 for _, project := range projects { 531 for _, projectID := range projectIDs { 532 if projectID == project.ID { 533 projectsMatched = append(projectsMatched, project) 534 break 535 } 536 } 537 } 538 539 return projectsMatched, nil 540 } 541 542 // GetProjectTokens returns the project tokens matching a given a slice of project names 543 func (s *System) GetProjectTokens(productToken string, projectNames []string) ([]string, error) { 544 projectTokens := []string{} 545 projects, err := s.GetProjectsMetaInfo(productToken) 546 if err != nil { 547 return nil, errors.Wrap(err, "failed to retrieve WhiteSource project meta info") 548 } 549 550 for _, project := range projects { 551 for _, projectName := range projectNames { 552 if projectName == project.Name { 553 projectTokens = append(projectTokens, project.Token) 554 } 555 } 556 } 557 558 if len(projectNames) > 0 && len(projectTokens) == 0 { 559 return projectTokens, fmt.Errorf("no project token(s) found for provided projects") 560 } 561 562 if len(projectNames) > 0 && len(projectNames) != len(projectTokens) { 563 return projectTokens, fmt.Errorf("not all project token(s) found for provided projects") 564 } 565 566 return projectTokens, nil 567 } 568 569 // GetProductName returns the product name for a given product token 570 func (s *System) GetProductName(productToken string) (string, error) { 571 wsResponse := struct { 572 ProductTags []Product `json:"productTags"` 573 }{ 574 ProductTags: []Product{}, 575 } 576 577 req := Request{ 578 RequestType: "getProductTags", 579 ProductToken: productToken, 580 } 581 582 err := s.sendRequestAndDecodeJSON(req, &wsResponse) 583 if err != nil { 584 return "", err 585 } 586 587 if len(wsResponse.ProductTags) == 0 { 588 return "", nil // fmt.Errorf("no product with token '%s' found in WhiteSource", productToken) 589 } 590 591 return wsResponse.ProductTags[0].Name, nil 592 } 593 594 // GetProjectRiskReport 595 func (s *System) GetProjectRiskReport(projectToken string) ([]byte, error) { 596 req := Request{ 597 RequestType: "getProjectRiskReport", 598 ProjectToken: projectToken, 599 } 600 601 respBody, err := s.sendRequest(req) 602 if err != nil { 603 return nil, errors.Wrap(err, "WhiteSource getProjectRiskReport request failed") 604 } 605 606 return respBody, nil 607 } 608 609 // GetProjectVulnerabilityReport 610 func (s *System) GetProjectVulnerabilityReport(projectToken string, format string) ([]byte, error) { 611 req := Request{ 612 RequestType: "getProjectVulnerabilityReport", 613 ProjectToken: projectToken, 614 Format: format, 615 } 616 617 respBody, err := s.sendRequest(req) 618 if err != nil { 619 return nil, errors.Wrap(err, "WhiteSource getProjectVulnerabilityReport request failed") 620 } 621 622 return respBody, nil 623 } 624 625 // GetProjectAlerts 626 func (s *System) GetProjectAlerts(projectToken string) ([]Alert, error) { 627 wsResponse := struct { 628 Alerts []Alert `json:"alerts"` 629 }{ 630 Alerts: []Alert{}, 631 } 632 633 req := Request{ 634 RequestType: "getProjectAlerts", 635 ProjectToken: projectToken, 636 } 637 638 err := s.sendRequestAndDecodeJSON(req, &wsResponse) 639 if err != nil { 640 return nil, err 641 } 642 643 return wsResponse.Alerts, nil 644 } 645 646 // GetProjectAlertsByType returns all alerts of a certain type for a given project 647 func (s *System) GetProjectAlertsByType(projectToken, alertType string) ([]Alert, error) { 648 wsResponse := struct { 649 Alerts []Alert `json:"alerts"` 650 }{ 651 Alerts: []Alert{}, 652 } 653 654 req := Request{ 655 RequestType: "getProjectAlertsByType", 656 ProjectToken: projectToken, 657 AlertType: alertType, 658 } 659 660 err := s.sendRequestAndDecodeJSON(req, &wsResponse) 661 if err != nil { 662 return nil, err 663 } 664 665 return wsResponse.Alerts, nil 666 } 667 668 // GetProjectIgnoredAlertsByType returns all ignored alerts of a certain type for a given project 669 func (s *System) GetProjectIgnoredAlertsByType(projectToken string, alertType string) ([]Alert, error) { 670 wsResponse := struct { 671 Alerts []Alert `json:"alerts"` 672 }{ 673 Alerts: []Alert{}, 674 } 675 676 req := Request{ 677 RequestType: "getProjectIgnoredAlerts", 678 ProjectToken: projectToken, 679 } 680 681 err := s.sendRequestAndDecodeJSON(req, &wsResponse) 682 if err != nil { 683 return nil, err 684 } 685 686 alerts := make([]Alert, 0) 687 for _, alert := range wsResponse.Alerts { 688 if alert.Type == alertType { 689 alerts = append(alerts, alert) 690 } 691 } 692 693 return alerts, nil 694 } 695 696 // GetProjectLibraryLocations 697 func (s *System) GetProjectLibraryLocations(projectToken string) ([]Library, error) { 698 wsResponse := struct { 699 Libraries []Library `json:"libraryLocations"` 700 }{ 701 Libraries: []Library{}, 702 } 703 704 req := Request{ 705 RequestType: "getProjectLibraryLocations", 706 ProjectToken: projectToken, 707 } 708 709 err := s.sendRequestAndDecodeJSON(req, &wsResponse) 710 if err != nil { 711 return nil, err 712 } 713 714 return wsResponse.Libraries, nil 715 } 716 717 func (s *System) sendRequestAndDecodeJSON(req Request, result interface{}) error { 718 var count int 719 return s.sendRequestAndDecodeJSONRecursive(req, result, &count) 720 } 721 722 func (s *System) sendRequestAndDecodeJSONRecursive(req Request, result interface{}, count *int) error { 723 respBody, err := s.sendRequest(req) 724 if err != nil { 725 return errors.Wrap(err, "sending whiteSource request failed") 726 } 727 728 log.Entry().Debugf("response: %v", string(respBody)) 729 730 errorResponse := struct { 731 ErrorCode int `json:"errorCode"` 732 ErrorMessage string `json:"errorMessage"` 733 }{} 734 735 err = json.Unmarshal(respBody, &errorResponse) 736 if err == nil && errorResponse.ErrorCode != 0 { 737 if *count < s.maxRetries && errorResponse.ErrorCode == 3000 { 738 var initial bool 739 if *count == 0 { 740 initial = true 741 } 742 log.Entry().Warnf("backend returned error 3000, retrying in %v", s.retryInterval) 743 time.Sleep(s.retryInterval) 744 *count = *count + 1 745 err = s.sendRequestAndDecodeJSONRecursive(req, result, count) 746 if err != nil { 747 if initial { 748 return errors.Wrapf(err, "WhiteSource request failed after %v retries", s.maxRetries) 749 } 750 return err 751 } 752 } 753 return fmt.Errorf("invalid request, error code %v, message '%s'", errorResponse.ErrorCode, errorResponse.ErrorMessage) 754 } 755 756 if result != nil { 757 err = json.Unmarshal(respBody, result) 758 if err != nil { 759 return errors.Wrap(err, "failed to parse WhiteSource response") 760 } 761 } 762 return nil 763 } 764 765 func (s *System) sendRequest(req Request) ([]byte, error) { 766 var responseBody []byte 767 if req.UserKey == "" { 768 req.UserKey = s.userToken 769 } 770 if req.OrgToken == "" { 771 req.OrgToken = s.orgToken 772 } 773 774 body, err := json.Marshal(req) 775 if err != nil { 776 return responseBody, errors.Wrap(err, "failed to create WhiteSource request") 777 } 778 779 log.Entry().Debugf("request: %v", string(body)) 780 781 headers := http.Header{} 782 headers.Add("Content-Type", "application/json") 783 response, err := s.httpClient.SendRequest(http.MethodPost, s.serverURL, bytes.NewBuffer(body), headers, nil) 784 if err != nil { 785 return responseBody, errors.Wrap(err, "failed to send request to WhiteSource") 786 } 787 defer response.Body.Close() 788 responseBody, err = io.ReadAll(response.Body) 789 if err != nil { 790 return responseBody, errors.Wrap(err, "failed to read WhiteSource response") 791 } 792 793 return responseBody, nil 794 }