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  }