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 }