github.com/jaylevin/jenkins-library@v1.230.4/pkg/whitesource/whitesource.go (about)

     1  package whitesource
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"net/http"
     9  	"time"
    10  
    11  	piperhttp "github.com/SAP/jenkins-library/pkg/http"
    12  	"github.com/SAP/jenkins-library/pkg/log"
    13  	"github.com/pkg/errors"
    14  )
    15  
    16  // ReportsDirectory defines the subfolder for the WhiteSource reports which are generated
    17  const ReportsDirectory = "whitesource"
    18  
    19  // Product defines a WhiteSource product with name and token
    20  type Product struct {
    21  	Name           string `json:"name"`
    22  	Token          string `json:"token"`
    23  	CreationDate   string `json:"creationDate,omitempty"`
    24  	LastUpdateDate string `json:"lastUpdatedDate,omitempty"`
    25  }
    26  
    27  // Assignment describes a list of UserAssignments and GroupAssignments which can be attributed to a WhiteSource Product.
    28  type Assignment struct {
    29  	UserAssignments  []UserAssignment  `json:"userAssignments,omitempty"`
    30  	GroupAssignments []GroupAssignment `json:"groupAssignments,omitempty"`
    31  }
    32  
    33  // UserAssignment holds an email address for a WhiteSource user
    34  // which can be assigned to a WhiteSource Product in a specific role.
    35  type UserAssignment struct {
    36  	Email string `json:"email,omitempty"`
    37  }
    38  
    39  // GroupAssignment refers to the name of a particular group in WhiteSource.
    40  type GroupAssignment struct {
    41  	Name string `json:"name,omitempty"`
    42  }
    43  
    44  // Alert
    45  type Alert struct {
    46  	Vulnerability    Vulnerability `json:"vulnerability"`
    47  	Type             string        `json:"type,omitempty"`
    48  	Level            string        `json:"level,omitempty"`
    49  	Library          Library       `json:"library,omitempty"`
    50  	Project          string        `json:"project,omitempty"`
    51  	DirectDependency bool          `json:"directDependency,omitempty"`
    52  	Description      string        `json:"description,omitempty"`
    53  	CreationDate     string        `json:"date,omitempty"`
    54  	ModifiedDate     string        `json:"modifiedDate,omitempty"`
    55  	Status           string        `json:"status,omitempty"`
    56  }
    57  
    58  // Title returns the issue title representation of the contents
    59  func (a Alert) Title() string {
    60  	return fmt.Sprintf("%v/%v/%v/%v", a.Type, consolidate(a.Vulnerability.Severity, a.Vulnerability.CVSS3Severity, a.Vulnerability.Score, a.Vulnerability.CVSS3Score), a.Vulnerability.Name, a.Library.ArtifactID)
    61  }
    62  
    63  func consolidate(cvss2severity, cvss3severity string, cvss2score, cvss3score float64) string {
    64  	switch cvss3severity {
    65  	case "low":
    66  		return "LOW"
    67  	case "medium":
    68  		return "MEDIUM"
    69  	case "high":
    70  		if cvss3score >= 9 {
    71  			return "CRITICAL"
    72  		}
    73  		return "HIGH"
    74  	}
    75  	switch cvss2severity {
    76  	case "low":
    77  		return "LOW"
    78  	case "medium":
    79  		return "MEDIUM"
    80  	case "high":
    81  		if cvss2score >= 9 {
    82  			return "CRITICAL"
    83  		}
    84  		return "HIGH"
    85  	}
    86  	return "none"
    87  }
    88  
    89  // ToMarkdown returns the markdown representation of the contents
    90  func (a Alert) ToMarkdown() ([]byte, error) {
    91  	score := a.Vulnerability.CVSS3Score
    92  	if score == 0 {
    93  		score = a.Vulnerability.Score
    94  	}
    95  	return []byte(fmt.Sprintf(
    96  		`**Vulnerability %v**
    97  | Severity | Base (NVD) Score | Temporal Score | Package | Installed Version | Description | Fix Resolution | Link |
    98  | --- | --- | --- | --- | --- | --- | --- | --- |
    99  |%v|%v|%v|%v|%v|%v|%v|[%v](%v)|
   100  `,
   101  		a.Vulnerability.Name,
   102  		a.Vulnerability.Severity,
   103  		score,
   104  		score,
   105  		a.Library.ArtifactID,
   106  		a.Library.Version,
   107  		a.Vulnerability.Description,
   108  		a.Vulnerability.TopFix.FixResolution,
   109  		a.Vulnerability.Name,
   110  		a.Vulnerability.URL,
   111  	)), nil
   112  }
   113  
   114  // ToTxt returns the textual representation of the contents
   115  func (a Alert) ToTxt() string {
   116  	score := a.Vulnerability.CVSS3Score
   117  	if score == 0 {
   118  		score = a.Vulnerability.Score
   119  	}
   120  	return fmt.Sprintf(`Vulnerability %v
   121  Severity: %v
   122  Base (NVD) Score: %v
   123  Temporal Score: %v
   124  Package: %v
   125  Installed Version: %v
   126  Description: %v
   127  Fix Resolution: %v
   128  Link: [%v](%v)`,
   129  		a.Vulnerability.Name,
   130  		a.Vulnerability.Severity,
   131  		score,
   132  		score,
   133  		a.Library.ArtifactID,
   134  		a.Library.Version,
   135  		a.Vulnerability.Description,
   136  		a.Vulnerability.TopFix.FixResolution,
   137  		a.Vulnerability.Name,
   138  		a.Vulnerability.URL,
   139  	)
   140  }
   141  
   142  // Library
   143  type Library struct {
   144  	Name       string `json:"name,omitempty"`
   145  	Filename   string `json:"filename,omitempty"`
   146  	ArtifactID string `json:"artifactId,omitempty"`
   147  	GroupID    string `json:"groupId,omitempty"`
   148  	Version    string `json:"version,omitempty"`
   149  }
   150  
   151  // Vulnerability defines a vulnerability as returned by WhiteSource
   152  type Vulnerability struct {
   153  	Name              string      `json:"name,omitempty"`
   154  	Type              string      `json:"type,omitempty"`
   155  	Severity          string      `json:"severity,omitempty"`
   156  	Score             float64     `json:"score,omitempty"`
   157  	CVSS3Severity     string      `json:"cvss3_severity,omitempty"`
   158  	CVSS3Score        float64     `json:"cvss3_score,omitempty"`
   159  	PublishDate       string      `json:"publishDate,omitempty"`
   160  	URL               string      `json:"url,omitempty"`
   161  	Description       string      `json:"description,omitempty"`
   162  	TopFix            Fix         `json:"topFix,omitempty"`
   163  	AllFixes          []Fix       `json:"allFixes,omitempty"`
   164  	FixResolutionText string      `json:"fixResolutionText,omitempty"`
   165  	References        []Reference `json:"references,omitempty"`
   166  }
   167  
   168  // Fix defines a Fix as returned by WhiteSource
   169  type Fix struct {
   170  	Vulnerability string `json:"vulnerability,omitempty"`
   171  	Type          string `json:"type,omitempty"`
   172  	Origin        string `json:"origin,omitempty"`
   173  	URL           string `json:"url,omitempty"`
   174  	FixResolution string `json:"fixResolution,omitempty"`
   175  	Date          string `json:"date,omitempty"`
   176  	Message       string `json:"message,omitempty"`
   177  	ExtraData     string `json:"extraData,omitempty"`
   178  }
   179  
   180  // Reference defines a reference for the library affected
   181  type Reference struct {
   182  	URL                 string `json:"url,omitempty"`
   183  	Homepage            string `json:"homepage,omitempty"`
   184  	GenericPackageIndex string `json:"genericPackageIndex,omitempty"`
   185  }
   186  
   187  // Project defines a WhiteSource project with name and token
   188  type Project struct {
   189  	ID             int64  `json:"id"`
   190  	Name           string `json:"name"`
   191  	PluginName     string `json:"pluginName"`
   192  	Token          string `json:"token"`
   193  	UploadedBy     string `json:"uploadedBy"`
   194  	CreationDate   string `json:"creationDate,omitempty"`
   195  	LastUpdateDate string `json:"lastUpdatedDate,omitempty"`
   196  }
   197  
   198  // Request defines a request object to be sent to the WhiteSource system
   199  type Request struct {
   200  	RequestType          string      `json:"requestType,omitempty"`
   201  	UserKey              string      `json:"userKey,omitempty"`
   202  	ProductToken         string      `json:"productToken,omitempty"`
   203  	ProductName          string      `json:"productName,omitempty"`
   204  	ProjectToken         string      `json:"projectToken,omitempty"`
   205  	OrgToken             string      `json:"orgToken,omitempty"`
   206  	Format               string      `json:"format,omitempty"`
   207  	AlertType            string      `json:"alertType,omitempty"`
   208  	ProductAdmins        *Assignment `json:"productAdmins,omitempty"`
   209  	ProductMembership    *Assignment `json:"productMembership,omitempty"`
   210  	AlertsEmailReceivers *Assignment `json:"alertsEmailReceivers,omitempty"`
   211  	ProductApprovers     *Assignment `json:"productApprovers,omitempty"`
   212  	ProductIntegrators   *Assignment `json:"productIntegrators,omitempty"`
   213  }
   214  
   215  // System defines a WhiteSource System including respective tokens (e.g. org token, user token)
   216  type System struct {
   217  	httpClient    piperhttp.Sender
   218  	orgToken      string
   219  	serverURL     string
   220  	userToken     string
   221  	maxRetries    int
   222  	retryInterval time.Duration
   223  }
   224  
   225  // DateTimeLayout is the layout of the time format used by the WhiteSource API.
   226  const DateTimeLayout = "2006-01-02 15:04:05 -0700"
   227  
   228  // NewSystem constructs a new System instance
   229  func NewSystem(serverURL, orgToken, userToken string, timeout time.Duration) *System {
   230  	httpClient := &piperhttp.Client{}
   231  	httpClient.SetOptions(piperhttp.ClientOptions{TransportTimeout: timeout})
   232  	return &System{
   233  		serverURL:     serverURL,
   234  		orgToken:      orgToken,
   235  		userToken:     userToken,
   236  		httpClient:    httpClient,
   237  		maxRetries:    10,
   238  		retryInterval: 3 * time.Second,
   239  	}
   240  }
   241  
   242  // GetProductsMetaInfo retrieves meta information for all WhiteSource products a user has access to
   243  func (s *System) GetProductsMetaInfo() ([]Product, error) {
   244  	wsResponse := struct {
   245  		ProductVitals []Product `json:"productVitals"`
   246  	}{
   247  		ProductVitals: []Product{},
   248  	}
   249  
   250  	req := Request{
   251  		RequestType: "getOrganizationProductVitals",
   252  	}
   253  
   254  	err := s.sendRequestAndDecodeJSON(req, &wsResponse)
   255  	if err != nil {
   256  		return wsResponse.ProductVitals, err
   257  	}
   258  
   259  	return wsResponse.ProductVitals, nil
   260  }
   261  
   262  // GetProductByName retrieves meta information for a specific WhiteSource product
   263  func (s *System) GetProductByName(productName string) (Product, error) {
   264  	products, err := s.GetProductsMetaInfo()
   265  	if err != nil {
   266  		return Product{}, errors.Wrap(err, "failed to retrieve WhiteSource products")
   267  	}
   268  
   269  	for _, p := range products {
   270  		if p.Name == productName {
   271  			return p, nil
   272  		}
   273  	}
   274  
   275  	return Product{}, fmt.Errorf("product '%v' not found in WhiteSource", productName)
   276  }
   277  
   278  // CreateProduct creates a new WhiteSource product and returns its product token.
   279  func (s *System) CreateProduct(productName string) (string, error) {
   280  	wsResponse := struct {
   281  		ProductToken string `json:"productToken"`
   282  	}{
   283  		ProductToken: "",
   284  	}
   285  
   286  	req := Request{
   287  		RequestType: "createProduct",
   288  		ProductName: productName,
   289  	}
   290  
   291  	err := s.sendRequestAndDecodeJSON(req, &wsResponse)
   292  	if err != nil {
   293  		return "", err
   294  	}
   295  
   296  	return wsResponse.ProductToken, nil
   297  }
   298  
   299  // SetProductAssignments assigns various types of membership to a WhiteSource Product.
   300  func (s *System) SetProductAssignments(productToken string, membership, admins, alertReceivers *Assignment) error {
   301  	req := Request{
   302  		RequestType:          "setProductAssignments",
   303  		ProductToken:         productToken,
   304  		ProductMembership:    membership,
   305  		ProductAdmins:        admins,
   306  		AlertsEmailReceivers: alertReceivers,
   307  	}
   308  
   309  	err := s.sendRequestAndDecodeJSON(req, nil)
   310  	if err != nil {
   311  		return err
   312  	}
   313  
   314  	return nil
   315  }
   316  
   317  // GetProjectsMetaInfo retrieves the registered projects for a specific WhiteSource product
   318  func (s *System) GetProjectsMetaInfo(productToken string) ([]Project, error) {
   319  	wsResponse := struct {
   320  		ProjectVitals []Project `json:"projectVitals"`
   321  	}{
   322  		ProjectVitals: []Project{},
   323  	}
   324  
   325  	req := Request{
   326  		RequestType:  "getProductProjectVitals",
   327  		ProductToken: productToken,
   328  	}
   329  
   330  	err := s.sendRequestAndDecodeJSON(req, &wsResponse)
   331  	if err != nil {
   332  		return nil, err
   333  	}
   334  
   335  	return wsResponse.ProjectVitals, nil
   336  }
   337  
   338  // GetProjectToken returns the project token for a project with a given name
   339  func (s *System) GetProjectToken(productToken, projectName string) (string, error) {
   340  	project, err := s.GetProjectByName(productToken, projectName)
   341  	if err != nil {
   342  		return "", err
   343  	}
   344  	return project.Token, nil
   345  }
   346  
   347  // GetProjectByToken returns project meta info given a project token
   348  func (s *System) GetProjectByToken(projectToken string) (Project, error) {
   349  	wsResponse := struct {
   350  		ProjectVitals []Project `json:"projectVitals"`
   351  	}{
   352  		ProjectVitals: []Project{},
   353  	}
   354  
   355  	req := Request{
   356  		RequestType:  "getProjectVitals",
   357  		ProjectToken: projectToken,
   358  	}
   359  
   360  	err := s.sendRequestAndDecodeJSON(req, &wsResponse)
   361  	if err != nil {
   362  		return Project{}, err
   363  	}
   364  
   365  	if len(wsResponse.ProjectVitals) == 0 {
   366  		return Project{}, errors.Wrapf(err, "no project with token '%s' found in WhiteSource", projectToken)
   367  	}
   368  
   369  	return wsResponse.ProjectVitals[0], nil
   370  }
   371  
   372  // GetProjectByName fetches all projects and returns the one matching the given projectName, or none, if not found
   373  func (s *System) GetProjectByName(productToken, projectName string) (Project, error) {
   374  	projects, err := s.GetProjectsMetaInfo(productToken)
   375  	if err != nil {
   376  		return Project{}, errors.Wrap(err, "failed to retrieve WhiteSource project meta info")
   377  	}
   378  
   379  	for _, project := range projects {
   380  		if projectName == project.Name {
   381  			return project, nil
   382  		}
   383  	}
   384  
   385  	// returns empty project and no error. The reason seems to be that it makes polling until the project exists easier.
   386  	return Project{}, nil
   387  }
   388  
   389  // GetProjectsByIDs retrieves all projects for the given productToken and filters them by the given project ids
   390  func (s *System) GetProjectsByIDs(productToken string, projectIDs []int64) ([]Project, error) {
   391  	projects, err := s.GetProjectsMetaInfo(productToken)
   392  	if err != nil {
   393  		return nil, errors.Wrap(err, "failed to retrieve WhiteSource project meta info")
   394  	}
   395  
   396  	var projectsMatched []Project
   397  	for _, project := range projects {
   398  		for _, projectID := range projectIDs {
   399  			if projectID == project.ID {
   400  				projectsMatched = append(projectsMatched, project)
   401  				break
   402  			}
   403  		}
   404  	}
   405  
   406  	return projectsMatched, nil
   407  }
   408  
   409  // GetProjectTokens returns the project tokens matching a given a slice of project names
   410  func (s *System) GetProjectTokens(productToken string, projectNames []string) ([]string, error) {
   411  	projectTokens := []string{}
   412  	projects, err := s.GetProjectsMetaInfo(productToken)
   413  	if err != nil {
   414  		return nil, errors.Wrap(err, "failed to retrieve WhiteSource project meta info")
   415  	}
   416  
   417  	for _, project := range projects {
   418  		for _, projectName := range projectNames {
   419  			if projectName == project.Name {
   420  				projectTokens = append(projectTokens, project.Token)
   421  			}
   422  		}
   423  	}
   424  
   425  	if len(projectNames) > 0 && len(projectTokens) == 0 {
   426  		return projectTokens, fmt.Errorf("no project token(s) found for provided projects")
   427  	}
   428  
   429  	if len(projectNames) > 0 && len(projectNames) != len(projectTokens) {
   430  		return projectTokens, fmt.Errorf("not all project token(s) found for provided projects")
   431  	}
   432  
   433  	return projectTokens, nil
   434  }
   435  
   436  // GetProductName returns the product name for a given product token
   437  func (s *System) GetProductName(productToken string) (string, error) {
   438  	wsResponse := struct {
   439  		ProductTags []Product `json:"productTags"`
   440  	}{
   441  		ProductTags: []Product{},
   442  	}
   443  
   444  	req := Request{
   445  		RequestType:  "getProductTags",
   446  		ProductToken: productToken,
   447  	}
   448  
   449  	err := s.sendRequestAndDecodeJSON(req, &wsResponse)
   450  	if err != nil {
   451  		return "", err
   452  	}
   453  
   454  	if len(wsResponse.ProductTags) == 0 {
   455  		return "", nil // fmt.Errorf("no product with token '%s' found in WhiteSource", productToken)
   456  	}
   457  
   458  	return wsResponse.ProductTags[0].Name, nil
   459  }
   460  
   461  // GetProjectRiskReport
   462  func (s *System) GetProjectRiskReport(projectToken string) ([]byte, error) {
   463  	req := Request{
   464  		RequestType:  "getProjectRiskReport",
   465  		ProjectToken: projectToken,
   466  	}
   467  
   468  	respBody, err := s.sendRequest(req)
   469  	if err != nil {
   470  		return nil, errors.Wrap(err, "WhiteSource getProjectRiskReport request failed")
   471  	}
   472  
   473  	return respBody, nil
   474  }
   475  
   476  // GetProjectVulnerabilityReport
   477  func (s *System) GetProjectVulnerabilityReport(projectToken string, format string) ([]byte, error) {
   478  	req := Request{
   479  		RequestType:  "getProjectVulnerabilityReport",
   480  		ProjectToken: projectToken,
   481  		Format:       format,
   482  	}
   483  
   484  	respBody, err := s.sendRequest(req)
   485  	if err != nil {
   486  		return nil, errors.Wrap(err, "WhiteSource getProjectVulnerabilityReport request failed")
   487  	}
   488  
   489  	return respBody, nil
   490  }
   491  
   492  // GetProjectAlerts
   493  func (s *System) GetProjectAlerts(projectToken string) ([]Alert, error) {
   494  	wsResponse := struct {
   495  		Alerts []Alert `json:"alerts"`
   496  	}{
   497  		Alerts: []Alert{},
   498  	}
   499  
   500  	req := Request{
   501  		RequestType:  "getProjectAlerts",
   502  		ProjectToken: projectToken,
   503  	}
   504  
   505  	err := s.sendRequestAndDecodeJSON(req, &wsResponse)
   506  	if err != nil {
   507  		return nil, err
   508  	}
   509  
   510  	return wsResponse.Alerts, nil
   511  }
   512  
   513  // GetProjectAlertsByType returns all alerts of a certain type for a given project
   514  func (s *System) GetProjectAlertsByType(projectToken, alertType string) ([]Alert, error) {
   515  	wsResponse := struct {
   516  		Alerts []Alert `json:"alerts"`
   517  	}{
   518  		Alerts: []Alert{},
   519  	}
   520  
   521  	req := Request{
   522  		RequestType:  "getProjectAlertsByType",
   523  		ProjectToken: projectToken,
   524  		AlertType:    alertType,
   525  	}
   526  
   527  	err := s.sendRequestAndDecodeJSON(req, &wsResponse)
   528  	if err != nil {
   529  		return nil, err
   530  	}
   531  
   532  	return wsResponse.Alerts, nil
   533  }
   534  
   535  // GetProjectLibraryLocations
   536  func (s *System) GetProjectLibraryLocations(projectToken string) ([]Library, error) {
   537  	wsResponse := struct {
   538  		Libraries []Library `json:"libraryLocations"`
   539  	}{
   540  		Libraries: []Library{},
   541  	}
   542  
   543  	req := Request{
   544  		RequestType:  "getProjectLibraryLocations",
   545  		ProjectToken: projectToken,
   546  	}
   547  
   548  	err := s.sendRequestAndDecodeJSON(req, &wsResponse)
   549  	if err != nil {
   550  		return nil, err
   551  	}
   552  
   553  	return wsResponse.Libraries, nil
   554  }
   555  
   556  func (s *System) sendRequestAndDecodeJSON(req Request, result interface{}) error {
   557  	var count int
   558  	return s.sendRequestAndDecodeJSONRecursive(req, result, &count)
   559  }
   560  
   561  func (s *System) sendRequestAndDecodeJSONRecursive(req Request, result interface{}, count *int) error {
   562  	respBody, err := s.sendRequest(req)
   563  	if err != nil {
   564  		return errors.Wrap(err, "sending whiteSource request failed")
   565  	}
   566  
   567  	log.Entry().Debugf("response: %v", string(respBody))
   568  
   569  	errorResponse := struct {
   570  		ErrorCode    int    `json:"errorCode"`
   571  		ErrorMessage string `json:"errorMessage"`
   572  	}{}
   573  
   574  	err = json.Unmarshal(respBody, &errorResponse)
   575  	if err == nil && errorResponse.ErrorCode != 0 {
   576  		if *count < s.maxRetries && errorResponse.ErrorCode == 3000 {
   577  			var initial bool
   578  			if *count == 0 {
   579  				initial = true
   580  			}
   581  			log.Entry().Warnf("backend returned error 3000, retrying in %v", s.retryInterval)
   582  			time.Sleep(s.retryInterval)
   583  			*count = *count + 1
   584  			err = s.sendRequestAndDecodeJSONRecursive(req, result, count)
   585  			if err != nil {
   586  				if initial {
   587  					return errors.Wrapf(err, "WhiteSource request failed after %v retries", s.maxRetries)
   588  				}
   589  				return err
   590  			}
   591  		}
   592  		return fmt.Errorf("invalid request, error code %v, message '%s'",
   593  			errorResponse.ErrorCode, errorResponse.ErrorMessage)
   594  	}
   595  
   596  	if result != nil {
   597  		err = json.Unmarshal(respBody, result)
   598  		if err != nil {
   599  			return errors.Wrap(err, "failed to parse WhiteSource response")
   600  		}
   601  	}
   602  	return nil
   603  }
   604  
   605  func (s *System) sendRequest(req Request) ([]byte, error) {
   606  	var responseBody []byte
   607  	if req.UserKey == "" {
   608  		req.UserKey = s.userToken
   609  	}
   610  	if req.OrgToken == "" {
   611  		req.OrgToken = s.orgToken
   612  	}
   613  
   614  	body, err := json.Marshal(req)
   615  	if err != nil {
   616  		return responseBody, errors.Wrap(err, "failed to create WhiteSource request")
   617  	}
   618  
   619  	log.Entry().Debugf("request: %v", string(body))
   620  
   621  	headers := http.Header{}
   622  	headers.Add("Content-Type", "application/json")
   623  	response, err := s.httpClient.SendRequest(http.MethodPost, s.serverURL, bytes.NewBuffer(body), headers, nil)
   624  	if err != nil {
   625  		return responseBody, errors.Wrap(err, "failed to send request to WhiteSource")
   626  	}
   627  	defer response.Body.Close()
   628  	responseBody, err = ioutil.ReadAll(response.Body)
   629  	if err != nil {
   630  		return responseBody, errors.Wrap(err, "failed to read WhiteSource response")
   631  	}
   632  
   633  	return responseBody, nil
   634  }