github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/runbits/requirements/requirements.go (about) 1 package requirements 2 3 import ( 4 "fmt" 5 "os" 6 "regexp" 7 "strconv" 8 "strings" 9 "time" 10 11 "github.com/ActiveState/cli/internal/analytics" 12 anaConsts "github.com/ActiveState/cli/internal/analytics/constants" 13 "github.com/ActiveState/cli/internal/captain" 14 "github.com/ActiveState/cli/internal/config" 15 "github.com/ActiveState/cli/internal/constants" 16 "github.com/ActiveState/cli/internal/errs" 17 "github.com/ActiveState/cli/internal/locale" 18 "github.com/ActiveState/cli/internal/logging" 19 configMediator "github.com/ActiveState/cli/internal/mediators/config" 20 "github.com/ActiveState/cli/internal/multilog" 21 "github.com/ActiveState/cli/internal/output" 22 "github.com/ActiveState/cli/internal/primer" 23 "github.com/ActiveState/cli/internal/prompt" 24 "github.com/ActiveState/cli/internal/rtutils/ptr" 25 "github.com/ActiveState/cli/internal/runbits" 26 "github.com/ActiveState/cli/internal/runbits/rationalize" 27 runbit "github.com/ActiveState/cli/internal/runbits/runtime" 28 "github.com/ActiveState/cli/pkg/buildplan" 29 "github.com/ActiveState/cli/pkg/localcommit" 30 "github.com/ActiveState/cli/pkg/platform/api/buildplanner/types" 31 medmodel "github.com/ActiveState/cli/pkg/platform/api/mediator/model" 32 vulnModel "github.com/ActiveState/cli/pkg/platform/api/vulnerabilities/model" 33 "github.com/ActiveState/cli/pkg/platform/api/vulnerabilities/request" 34 "github.com/ActiveState/cli/pkg/platform/authentication" 35 "github.com/ActiveState/cli/pkg/platform/model" 36 bpModel "github.com/ActiveState/cli/pkg/platform/model/buildplanner" 37 "github.com/ActiveState/cli/pkg/platform/runtime/buildscript" 38 "github.com/ActiveState/cli/pkg/platform/runtime/target" 39 "github.com/ActiveState/cli/pkg/project" 40 "github.com/ActiveState/cli/pkg/sysinfo" 41 "github.com/go-openapi/strfmt" 42 "github.com/thoas/go-funk" 43 ) 44 45 func init() { 46 configMediator.RegisterOption(constants.SecurityPromptConfig, configMediator.Bool, true) 47 configMediator.RegisterOption(constants.SecurityPromptLevelConfig, configMediator.String, vulnModel.SeverityCritical) 48 } 49 50 type PackageVersion struct { 51 captain.NameVersionValue 52 } 53 54 func (pv *PackageVersion) Set(arg string) error { 55 err := pv.NameVersionValue.Set(arg) 56 if err != nil { 57 return locale.WrapInputError(err, "err_package_format", "The package and version provided is not formatting correctly, must be in the form of <package>@<version>") 58 } 59 return nil 60 } 61 62 type RequirementOperation struct { 63 Output output.Outputer 64 Prompt prompt.Prompter 65 Project *project.Project 66 Auth *authentication.Auth 67 Config *config.Instance 68 Analytics analytics.Dispatcher 69 SvcModel *model.SvcModel 70 } 71 72 type primeable interface { 73 primer.Outputer 74 primer.Prompter 75 primer.Projecter 76 primer.Auther 77 primer.Configurer 78 primer.Analyticer 79 primer.SvcModeler 80 } 81 82 func NewRequirementOperation(prime primeable) *RequirementOperation { 83 return &RequirementOperation{ 84 prime.Output(), 85 prime.Prompt(), 86 prime.Project(), 87 prime.Auth(), 88 prime.Config(), 89 prime.Analytics(), 90 prime.SvcModel(), 91 } 92 } 93 94 const latestVersion = "latest" 95 96 type ErrNoMatches struct { 97 *locale.LocalizedError 98 Query string 99 Alternatives *string 100 } 101 102 var errNoRequirements = errs.New("No requirements were provided") 103 104 var errInitialNoRequirement = errs.New("Could not find compatible requirement for initial commit") 105 106 var versionRe = regexp.MustCompile(`^\d(\.\d+)*$`) 107 108 // Requirement represents a package, language or platform requirement 109 // For now, be aware that you should never provide BOTH ns AND nsType, one or the other should always be nil, but never both. 110 // The refactor should clean this up. 111 type Requirement struct { 112 Name string 113 Version string 114 Revision *int 115 BitWidth int // Only needed for platform requirements 116 Namespace *model.Namespace 117 NamespaceType *model.NamespaceType 118 Operation types.Operation 119 120 // The following fields are set during execution 121 langName string 122 langVersion string 123 validatePkg bool 124 appendVersionWildcard bool 125 originalRequirementName string 126 versionRequirements []types.VersionRequirement 127 } 128 129 // ExecuteRequirementOperation executes the operation on the requirement 130 // This has become quite unwieldy, and is ripe for a refactor - https://activestatef.atlassian.net/browse/DX-1897 131 func (r *RequirementOperation) ExecuteRequirementOperation(ts *time.Time, requirements ...*Requirement) (rerr error) { 132 defer r.rationalizeError(&rerr) 133 134 if len(requirements) == 0 { 135 return errNoRequirements 136 } 137 138 out := r.Output 139 var pg *output.Spinner 140 defer func() { 141 if pg != nil { 142 // This is a bit awkward, but it would be even more awkward to manually address this for every error condition 143 pg.Stop(locale.T("progress_fail")) 144 } 145 }() 146 147 if r.Project == nil { 148 return rationalize.ErrNoProject 149 } 150 if r.Project.IsHeadless() { 151 return rationalize.ErrHeadless 152 } 153 out.Notice(locale.Tr("operating_message", r.Project.NamespaceString(), r.Project.Dir())) 154 155 if err := r.resolveNamespaces(ts, requirements...); err != nil { 156 return errs.Wrap(err, "Could not resolve namespaces") 157 } 158 159 if err := r.validatePackages(requirements...); err != nil { 160 return errs.Wrap(err, "Could not validate packages") 161 } 162 163 parentCommitID, err := localcommit.Get(r.Project.Dir()) 164 if err != nil { 165 return errs.Wrap(err, "Unable to get local commit") 166 } 167 hasParentCommit := parentCommitID != "" 168 169 pg = output.StartSpinner(out, locale.T("progress_commit"), constants.TerminalAnimationInterval) 170 171 if err := r.checkForUpdate(parentCommitID, requirements...); err != nil { 172 return locale.WrapError(err, "err_check_for_update", "Could not check for requirements updates") 173 } 174 175 if !hasParentCommit { 176 // Use first requirement to extract language for initial commit 177 var requirement *Requirement 178 for _, r := range requirements { 179 if r.Namespace.Type() == model.NamespacePackage || r.Namespace.Type() == model.NamespaceBundle { 180 requirement = r 181 break 182 } 183 } 184 185 if requirement == nil { 186 return errInitialNoRequirement 187 } 188 189 languageFromNs := model.LanguageFromNamespace(requirement.Namespace.String()) 190 parentCommitID, err = model.CommitInitial(sysinfo.OS().String(), languageFromNs, requirement.langVersion, r.Auth) 191 if err != nil { 192 return locale.WrapError(err, "err_install_no_project_commit", "Could not create initial commit for new project") 193 } 194 } 195 196 if err := r.resolveRequirements(requirements...); err != nil { 197 return locale.WrapError(err, "err_resolve_requirements", "Could not resolve one or more requirements") 198 } 199 200 var stageCommitReqs []bpModel.StageCommitRequirement 201 for _, requirement := range requirements { 202 stageCommitReqs = append(stageCommitReqs, bpModel.StageCommitRequirement{ 203 Name: requirement.Name, 204 Version: requirement.versionRequirements, 205 Revision: requirement.Revision, 206 Namespace: requirement.Namespace.String(), 207 Operation: requirement.Operation, 208 }) 209 } 210 211 params := bpModel.StageCommitParams{ 212 Owner: r.Project.Owner(), 213 Project: r.Project.Name(), 214 ParentCommit: string(parentCommitID), 215 Description: commitMessage(requirements...), 216 Requirements: stageCommitReqs, 217 TimeStamp: ts, 218 } 219 220 bp := bpModel.NewBuildPlannerModel(r.Auth) 221 commitID, err := bp.StageCommit(params) 222 if err != nil { 223 return locale.WrapError(err, "err_package_save_and_build", "Error occurred while trying to create a commit") 224 } 225 226 pg.Stop(locale.T("progress_success")) 227 pg = nil 228 229 if strings.ToLower(os.Getenv(constants.DisableRuntime)) != "true" { 230 ns := requirements[0].Namespace 231 var trigger target.Trigger 232 switch ns.Type() { 233 case model.NamespaceLanguage: 234 trigger = target.TriggerLanguage 235 case model.NamespacePlatform: 236 trigger = target.TriggerPlatform 237 default: 238 trigger = target.TriggerPackage 239 } 240 241 // Solve runtime 242 rt, rtCommit, err := runbit.Solve(r.Auth, r.Output, r.Analytics, r.Project, &commitID, trigger, r.SvcModel, r.Config, runbit.OptNone) 243 if err != nil { 244 return errs.Wrap(err, "Could not solve runtime") 245 } 246 247 // Get old buildplan 248 // We can't use the local store here; because it might not exist (ie. integrationt test, user cleaned cache, ..), 249 // but also there's no guarantee the old one is sequential to the current. 250 oldCommit, err := model.GetCommit(commitID, r.Auth) 251 if err != nil { 252 return errs.Wrap(err, "Could not get commit") 253 } 254 255 var oldBuildPlan *buildplan.BuildPlan 256 rtTarget := target.NewProjectTarget(r.Project, &commitID, trigger) 257 if oldCommit.ParentCommitID != "" { 258 bpm := bpModel.NewBuildPlannerModel(r.Auth) 259 commit, err := bpm.FetchCommit(oldCommit.ParentCommitID, rtTarget.Owner(), rtTarget.Name(), nil) 260 if err != nil { 261 return errs.Wrap(err, "Failed to fetch build result") 262 } 263 oldBuildPlan = commit.BuildPlan() 264 } 265 266 changedArtifacts := rtCommit.BuildPlan().DiffArtifacts(oldBuildPlan, false) 267 268 // Report CVEs 269 if err := r.cveReport(changedArtifacts, requirements...); err != nil { 270 return errs.Wrap(err, "Could not report CVEs") 271 } 272 273 // Start runtime update UI 274 if !r.Config.GetBool(constants.AsyncRuntimeConfig) { 275 out.Notice("") 276 if !rt.HasCache() { 277 out.Notice(output.Title(locale.T("install_runtime"))) 278 out.Notice(locale.T("install_runtime_info")) 279 } else { 280 out.Notice(output.Title(locale.T("update_runtime"))) 281 out.Notice(locale.T("update_runtime_info")) 282 } 283 284 // refresh or install runtime 285 err = runbit.UpdateByReference(rt, rtCommit, r.Auth, r.Project, r.Output) 286 if err != nil { 287 if !runbits.IsBuildError(err) { 288 // If the error is not a build error we want to retain the changes 289 if err2 := r.updateCommitID(commitID); err2 != nil { 290 return errs.Pack(err, locale.WrapError(err2, "err_package_update_commit_id")) 291 } 292 } 293 return errs.Wrap(err, "Failed to refresh runtime") 294 } 295 } 296 } 297 298 if err := r.updateCommitID(commitID); err != nil { 299 return locale.WrapError(err, "err_package_update_commit_id") 300 } 301 302 if !hasParentCommit { 303 out.Notice(locale.Tr("install_initial_success", r.Project.Source().Path())) 304 } 305 306 // Print the result 307 r.outputResults(requirements...) 308 309 out.Notice(locale.T("operation_success_local")) 310 311 return nil 312 } 313 314 type ResolveNamespaceError struct { 315 error 316 Name string 317 } 318 319 func (r *RequirementOperation) resolveNamespaces(ts *time.Time, requirements ...*Requirement) error { 320 for _, requirement := range requirements { 321 if err := r.resolveNamespace(ts, requirement); err != nil { 322 return &ResolveNamespaceError{ 323 err, 324 requirement.Name, 325 } 326 } 327 } 328 return nil 329 } 330 331 func (r *RequirementOperation) resolveNamespace(ts *time.Time, requirement *Requirement) error { 332 requirement.langName = "undetermined" 333 334 if requirement.NamespaceType != nil { 335 switch *requirement.NamespaceType { 336 case model.NamespacePackage, model.NamespaceBundle: 337 commitID, err := localcommit.Get(r.Project.Dir()) 338 if err != nil { 339 return errs.Wrap(err, "Unable to get local commit") 340 } 341 342 language, err := model.LanguageByCommit(commitID, r.Auth) 343 if err == nil { 344 requirement.langName = language.Name 345 requirement.Namespace = ptr.To(model.NewNamespacePkgOrBundle(requirement.langName, *requirement.NamespaceType)) 346 } else { 347 logging.Debug("Could not get language from project: %v", err) 348 } 349 case model.NamespaceLanguage: 350 requirement.Namespace = ptr.To(model.NewNamespaceLanguage()) 351 case model.NamespacePlatform: 352 requirement.Namespace = ptr.To(model.NewNamespacePlatform()) 353 } 354 } 355 356 ns := requirement.Namespace 357 nsType := requirement.NamespaceType 358 requirement.validatePkg = requirement.Operation == types.OperationAdded && ns != nil && (ns.Type() == model.NamespacePackage || ns.Type() == model.NamespaceBundle || ns.Type() == model.NamespaceLanguage) 359 if (ns == nil || !ns.IsValid()) && nsType != nil && (*nsType == model.NamespacePackage || *nsType == model.NamespaceBundle) { 360 pg := output.StartSpinner(r.Output, locale.Tr("progress_pkg_nolang", requirement.Name), constants.TerminalAnimationInterval) 361 362 supported, err := model.FetchSupportedLanguages(sysinfo.OS().String()) 363 if err != nil { 364 return errs.Wrap(err, "Failed to retrieve the list of supported languages") 365 } 366 367 var nsv model.Namespace 368 var supportedLang *medmodel.SupportedLanguage 369 requirement.Name, nsv, supportedLang, err = resolvePkgAndNamespace(r.Prompt, requirement.Name, *requirement.NamespaceType, supported, ts, r.Auth) 370 if err != nil { 371 return errs.Wrap(err, "Could not resolve pkg and namespace") 372 } 373 requirement.Namespace = &nsv 374 requirement.langVersion = supportedLang.DefaultVersion 375 requirement.langName = supportedLang.Name 376 377 requirement.validatePkg = false 378 379 pg.Stop(locale.T("progress_found")) 380 } 381 382 if requirement.Namespace == nil { 383 return locale.NewError("err_package_invalid_namespace_detected", "No valid namespace could be detected") 384 } 385 386 return nil 387 } 388 389 func (r *RequirementOperation) validatePackages(requirements ...*Requirement) error { 390 var requirementsToValidate []*Requirement 391 for _, requirement := range requirements { 392 if !requirement.validatePkg { 393 continue 394 } 395 requirementsToValidate = append(requirementsToValidate, requirement) 396 } 397 398 if len(requirementsToValidate) == 0 { 399 return nil 400 } 401 402 pg := output.StartSpinner(r.Output, locale.Tr("progress_search", strings.Join(requirementNames(requirementsToValidate...), ", ")), constants.TerminalAnimationInterval) 403 for _, requirement := range requirementsToValidate { 404 if err := r.validatePackage(requirement); err != nil { 405 return errs.Wrap(err, "Could not validate package") 406 } 407 } 408 pg.Stop(locale.T("progress_found")) 409 410 return nil 411 } 412 413 func (r *RequirementOperation) validatePackage(requirement *Requirement) error { 414 if strings.ToLower(requirement.Version) == latestVersion { 415 requirement.Version = "" 416 } 417 418 requirement.originalRequirementName = requirement.Name 419 normalized, err := model.FetchNormalizedName(*requirement.Namespace, requirement.Name, r.Auth) 420 if err != nil { 421 multilog.Error("Failed to normalize '%s': %v", requirement.Name, err) 422 } 423 424 packages, err := model.SearchIngredientsStrict(requirement.Namespace.String(), normalized, false, false, nil, r.Auth) // ideally case-sensitive would be true (PB-4371) 425 if err != nil { 426 return locale.WrapError(err, "package_err_cannot_obtain_search_results") 427 } 428 429 if len(packages) == 0 { 430 suggestions, err := getSuggestions(*requirement.Namespace, requirement.Name, r.Auth) 431 if err != nil { 432 multilog.Error("Failed to retrieve suggestions: %v", err) 433 } 434 435 if len(suggestions) == 0 { 436 return &ErrNoMatches{ 437 locale.WrapExternalError(err, "package_ingredient_alternatives_nosuggest", "", requirement.Name), 438 requirement.Name, nil} 439 } 440 441 return &ErrNoMatches{ 442 locale.WrapExternalError(err, "package_ingredient_alternatives", "", requirement.Name, strings.Join(suggestions, "\n")), 443 requirement.Name, ptr.To(strings.Join(suggestions, "\n"))} 444 } 445 446 if normalized != "" && normalized != requirement.Name { 447 requirement.Name = normalized 448 } 449 450 // If a bare version number was given, and if it is a partial version number (e.g. requests@2), 451 // we'll want to ultimately append a '.x' suffix. 452 if versionRe.MatchString(requirement.Version) { 453 for _, knownVersion := range packages[0].Versions { 454 if knownVersion.Version == requirement.Version { 455 break 456 } else if strings.HasPrefix(knownVersion.Version, requirement.Version) { 457 requirement.appendVersionWildcard = true 458 } 459 } 460 } 461 462 return nil 463 } 464 465 func (r *RequirementOperation) checkForUpdate(parentCommitID strfmt.UUID, requirements ...*Requirement) error { 466 for _, requirement := range requirements { 467 // Check if this is an addition or an update 468 if requirement.Operation == types.OperationAdded && parentCommitID != "" { 469 req, err := model.GetRequirement(parentCommitID, *requirement.Namespace, requirement.Name, r.Auth) 470 if err != nil { 471 return errs.Wrap(err, "Could not get requirement") 472 } 473 if req != nil { 474 requirement.Operation = types.OperationUpdated 475 } 476 } 477 478 r.Analytics.EventWithLabel( 479 anaConsts.CatPackageOp, fmt.Sprintf("%s-%s", requirement.Operation, requirement.langName), requirement.Name, 480 ) 481 } 482 483 return nil 484 } 485 486 func (r *RequirementOperation) resolveRequirements(requirements ...*Requirement) error { 487 for _, requirement := range requirements { 488 if err := r.resolveRequirement(requirement); err != nil { 489 return errs.Wrap(err, "Could not resolve requirement") 490 } 491 } 492 return nil 493 } 494 495 func (r *RequirementOperation) resolveRequirement(requirement *Requirement) error { 496 var err error 497 requirement.Name, requirement.Version, err = model.ResolveRequirementNameAndVersion(requirement.Name, requirement.Version, requirement.BitWidth, *requirement.Namespace, r.Auth) 498 if err != nil { 499 return errs.Wrap(err, "Could not resolve requirement name and version") 500 } 501 502 versionString := requirement.Version 503 if requirement.appendVersionWildcard { 504 versionString += ".x" 505 } 506 507 requirement.versionRequirements, err = bpModel.VersionStringToRequirements(versionString) 508 if err != nil { 509 return errs.Wrap(err, "Could not process version string into requirements") 510 } 511 512 return nil 513 } 514 515 func (r *RequirementOperation) cveReport(artifactChangeset buildplan.ArtifactChangeset, requirements ...*Requirement) error { 516 if r.shouldSkipCVEs(requirements...) { 517 logging.Debug("Skipping CVE reporting") 518 return nil 519 } 520 521 names := requirementNames(requirements...) 522 pg := output.StartSpinner(r.Output, locale.Tr("progress_cve_search", strings.Join(names, ", ")), constants.TerminalAnimationInterval) 523 524 var ingredients []*request.Ingredient 525 for _, requirement := range requirements { 526 if requirement.Operation == types.OperationRemoved { 527 continue 528 } 529 530 for _, artifact := range artifactChangeset.Added { 531 for _, ing := range artifact.Ingredients { 532 ingredients = append(ingredients, &request.Ingredient{ 533 Namespace: ing.Namespace, 534 Name: ing.Name, 535 Version: ing.Version, 536 }) 537 } 538 } 539 540 for _, change := range artifactChangeset.Updated { 541 if !change.VersionsChanged() { 542 continue // For CVE reporting we only care about ingredient changes 543 } 544 545 for _, ing := range change.To.Ingredients { 546 ingredients = append(ingredients, &request.Ingredient{ 547 Namespace: ing.Namespace, 548 Name: ing.Name, 549 Version: ing.Version, 550 }) 551 } 552 } 553 } 554 555 ingredientVulnerabilities, err := model.FetchVulnerabilitiesForIngredients(r.Auth, ingredients) 556 if err != nil { 557 return errs.Wrap(err, "Failed to retrieve vulnerabilities") 558 } 559 560 // No vulnerabilities, nothing further to do here 561 if len(ingredientVulnerabilities) == 0 { 562 logging.Debug("No vulnerabilities found for ingredients") 563 pg.Stop(locale.T("progress_safe")) 564 pg = nil 565 return nil 566 } 567 568 pg.Stop(locale.T("progress_unsafe")) 569 pg = nil 570 571 vulnerabilities := model.CombineVulnerabilities(ingredientVulnerabilities, names...) 572 r.summarizeCVEs(r.Output, vulnerabilities) 573 574 if r.shouldPromptForSecurity(vulnerabilities) { 575 cont, err := r.promptForSecurity() 576 if err != nil { 577 return errs.Wrap(err, "Failed to prompt for security") 578 } 579 580 if !cont { 581 if !r.Prompt.IsInteractive() { 582 return errs.AddTips( 583 locale.NewInputError("err_pkgop_security_prompt", "Operation aborted due to security prompt"), 584 locale.Tl("more_info_prompt", "To disable security prompting run: [ACTIONABLE]state config set security.prompt.enabled false[/RESET]"), 585 ) 586 } 587 return locale.NewInputError("err_pkgop_security_prompt", "Operation aborted due to security prompt") 588 } 589 } 590 591 return nil 592 } 593 594 func (r *RequirementOperation) shouldSkipCVEs(requirements ...*Requirement) bool { 595 if !r.Auth.Authenticated() { 596 return true 597 } 598 599 for _, req := range requirements { 600 if req.Operation != types.OperationRemoved { 601 return false 602 } 603 } 604 605 return true 606 } 607 608 func (r *RequirementOperation) updateCommitID(commitID strfmt.UUID) error { 609 if err := localcommit.Set(r.Project.Dir(), commitID.String()); err != nil { 610 return locale.WrapError(err, "err_package_update_commit_id") 611 } 612 613 if r.Config.GetBool(constants.OptinBuildscriptsConfig) { 614 bp := bpModel.NewBuildPlannerModel(r.Auth) 615 expr, atTime, err := bp.GetBuildExpressionAndTime(commitID.String()) 616 if err != nil { 617 return errs.Wrap(err, "Could not get remote build expr and time") 618 } 619 620 err = buildscript.Update(r.Project, atTime, expr) 621 if err != nil { 622 return locale.WrapError(err, "err_update_build_script") 623 } 624 } 625 626 return nil 627 } 628 629 func (r *RequirementOperation) shouldPromptForSecurity(vulnerabilities model.VulnerableIngredientsByLevels) bool { 630 if !r.Config.GetBool(constants.SecurityPromptConfig) || vulnerabilities.Count == 0 { 631 return false 632 } 633 634 promptLevel := r.Config.GetString(constants.SecurityPromptLevelConfig) 635 636 logging.Debug("Prompt level: ", promptLevel) 637 switch promptLevel { 638 case vulnModel.SeverityCritical: 639 return vulnerabilities.Critical.Count > 0 640 case vulnModel.SeverityHigh: 641 return vulnerabilities.Critical.Count > 0 || 642 vulnerabilities.High.Count > 0 643 case vulnModel.SeverityMedium: 644 return vulnerabilities.Critical.Count > 0 || 645 vulnerabilities.High.Count > 0 || 646 vulnerabilities.Medium.Count > 0 647 case vulnModel.SeverityLow: 648 return vulnerabilities.Critical.Count > 0 || 649 vulnerabilities.High.Count > 0 || 650 vulnerabilities.Medium.Count > 0 || 651 vulnerabilities.Low.Count > 0 652 } 653 654 return false 655 } 656 657 func (r *RequirementOperation) summarizeCVEs(out output.Outputer, vulnerabilities model.VulnerableIngredientsByLevels) { 658 out.Print("") 659 660 switch { 661 case vulnerabilities.CountPrimary == 0: 662 out.Print(locale.Tr("warning_vulnerable_indirectonly", strconv.Itoa(vulnerabilities.Count))) 663 case vulnerabilities.CountPrimary == vulnerabilities.Count: 664 out.Print(locale.Tr("warning_vulnerable_directonly", strconv.Itoa(vulnerabilities.Count))) 665 default: 666 out.Print(locale.Tr("warning_vulnerable", strconv.Itoa(vulnerabilities.CountPrimary), strconv.Itoa(vulnerabilities.Count-vulnerabilities.CountPrimary))) 667 } 668 669 printVulnerabilities := func(vulnerableIngredients model.VulnerableIngredientsByLevel, name, color string) { 670 if vulnerableIngredients.Count > 0 { 671 ings := []string{} 672 for _, vulns := range vulnerableIngredients.Ingredients { 673 prefix := "" 674 if vulnerabilities.Count > vulnerabilities.CountPrimary { 675 prefix = fmt.Sprintf("%s@%s: ", vulns.IngredientName, vulns.IngredientVersion) 676 } 677 ings = append(ings, fmt.Sprintf("%s[CYAN]%s[/RESET]", prefix, strings.Join(vulns.CVEIDs, ", "))) 678 } 679 out.Print(fmt.Sprintf(" • [%s]%d %s:[/RESET] %s", color, vulnerableIngredients.Count, name, strings.Join(ings, ", "))) 680 } 681 } 682 683 printVulnerabilities(vulnerabilities.Critical, locale.Tl("cve_critical", "Critical"), "RED") 684 printVulnerabilities(vulnerabilities.High, locale.Tl("cve_high", "High"), "ORANGE") 685 printVulnerabilities(vulnerabilities.Medium, locale.Tl("cve_medium", "Medium"), "YELLOW") 686 printVulnerabilities(vulnerabilities.Low, locale.Tl("cve_low", "Low"), "MAGENTA") 687 688 out.Print("") 689 out.Print(locale.T("more_info_vulnerabilities")) 690 } 691 692 func (r *RequirementOperation) promptForSecurity() (bool, error) { 693 confirm, err := r.Prompt.Confirm("", locale.Tr("prompt_continue_pkg_operation"), ptr.To(false)) 694 if err != nil { 695 return false, locale.WrapError(err, "err_pkgop_confirm", "Need a confirmation.") 696 } 697 698 return confirm, nil 699 } 700 701 func (r *RequirementOperation) outputResults(requirements ...*Requirement) { 702 for _, requirement := range requirements { 703 r.outputResult(requirement) 704 } 705 } 706 707 func (r *RequirementOperation) outputResult(requirement *Requirement) { 708 // Print the result 709 message := locale.Tr(fmt.Sprintf("%s_version_%s", requirement.Namespace.Type(), requirement.Operation), requirement.Name, requirement.Version) 710 if requirement.Version == "" { 711 message = locale.Tr(fmt.Sprintf("%s_%s", requirement.Namespace.Type(), requirement.Operation), requirement.Name) 712 } 713 714 r.Output.Print(output.Prepare( 715 message, 716 &struct { 717 Name string `json:"name"` 718 Version string `json:"version,omitempty"` 719 Type string `json:"type"` 720 Operation string `json:"operation"` 721 }{ 722 requirement.Name, 723 requirement.Version, 724 requirement.Namespace.Type().String(), 725 requirement.Operation.String(), 726 })) 727 728 if requirement.originalRequirementName != requirement.Name && requirement.Operation != types.OperationRemoved { 729 r.Output.Notice(locale.Tl("package_version_differs", 730 "Note: the actual package name ({{.V0}}) is different from the requested package name ({{.V1}})", 731 requirement.Name, requirement.originalRequirementName)) 732 } 733 } 734 735 func supportedLanguageByName(supported []medmodel.SupportedLanguage, langName string) medmodel.SupportedLanguage { 736 return funk.Find(supported, func(l medmodel.SupportedLanguage) bool { return l.Name == langName }).(medmodel.SupportedLanguage) 737 } 738 739 func resolvePkgAndNamespace(prompt prompt.Prompter, packageName string, nsType model.NamespaceType, supported []medmodel.SupportedLanguage, ts *time.Time, auth *authentication.Auth) (string, model.Namespace, *medmodel.SupportedLanguage, error) { 740 ns := model.NewBlankNamespace() 741 742 // Find ingredients that match the input query 743 ingredients, err := model.SearchIngredientsStrict("", packageName, false, false, ts, auth) 744 if err != nil { 745 return "", ns, nil, locale.WrapError(err, "err_pkgop_search_err", "Failed to check for ingredients.") 746 } 747 748 ingredients, err = model.FilterSupportedIngredients(supported, ingredients) 749 if err != nil { 750 return "", ns, nil, errs.Wrap(err, "Failed to filter out unsupported packages") 751 } 752 753 choices := []string{} 754 values := map[string][]string{} 755 for _, i := range ingredients { 756 language := model.LanguageFromNamespace(*i.Ingredient.PrimaryNamespace) 757 758 // Generate ingredient choices to present to the user 759 name := fmt.Sprintf("%s (%s)", *i.Ingredient.Name, language) 760 choices = append(choices, name) 761 values[name] = []string{*i.Ingredient.Name, language} 762 } 763 764 if len(choices) == 0 { 765 return "", ns, nil, locale.WrapExternalError(err, "package_ingredient_alternatives_nolang", "", packageName) 766 } 767 768 // If we only have one ingredient match we're done; return it. 769 if len(choices) == 1 { 770 language := values[choices[0]][1] 771 supportedLang := supportedLanguageByName(supported, language) 772 return values[choices[0]][0], model.NewNamespacePkgOrBundle(language, nsType), &supportedLang, nil 773 } 774 775 // Prompt the user with the ingredient choices 776 choice, err := prompt.Select( 777 locale.Tl("prompt_pkgop_ingredient", "Multiple Matches"), 778 locale.Tl("prompt_pkgop_ingredient_msg", "Your query has multiple matches, which one would you like to use?"), 779 choices, &choices[0], 780 ) 781 if err != nil { 782 return "", ns, nil, locale.WrapError(err, "err_pkgop_select", "Need a selection.") 783 } 784 785 // Return the user selected ingredient 786 language := values[choice][1] 787 supportedLang := supportedLanguageByName(supported, language) 788 return values[choice][0], model.NewNamespacePkgOrBundle(language, nsType), &supportedLang, nil 789 } 790 791 func getSuggestions(ns model.Namespace, name string, auth *authentication.Auth) ([]string, error) { 792 results, err := model.SearchIngredients(ns.String(), name, false, nil, auth) 793 if err != nil { 794 return []string{}, locale.WrapError(err, "package_ingredient_err_search", "Failed to resolve ingredient named: {{.V0}}", name) 795 } 796 797 maxResults := 5 798 if len(results) > maxResults { 799 results = results[:maxResults] 800 } 801 802 suggestions := make([]string, 0, maxResults+1) 803 for _, result := range results { 804 suggestions = append(suggestions, fmt.Sprintf(" - %s", *result.Ingredient.Name)) 805 } 806 807 return suggestions, nil 808 } 809 810 func commitMessage(requirements ...*Requirement) string { 811 switch len(requirements) { 812 case 0: 813 return "" 814 case 1: 815 return requirementCommitMessage(requirements[0]) 816 default: 817 return commitMessageMultiple(requirements...) 818 } 819 } 820 821 func requirementCommitMessage(req *Requirement) string { 822 switch req.Namespace.Type() { 823 case model.NamespaceLanguage: 824 return languageCommitMessage(req.Operation, req.Name, req.Version) 825 case model.NamespacePlatform: 826 return platformCommitMessage(req.Operation, req.Name, req.Version, req.BitWidth) 827 case model.NamespacePackage, model.NamespaceBundle: 828 return packageCommitMessage(req.Operation, req.Name, req.Version) 829 } 830 return "" 831 } 832 833 func languageCommitMessage(op types.Operation, name, version string) string { 834 var msgL10nKey string 835 switch op { 836 case types.OperationAdded: 837 msgL10nKey = "commit_message_added_language" 838 case types.OperationUpdated: 839 msgL10nKey = "commit_message_updated_language" 840 case types.OperationRemoved: 841 msgL10nKey = "commit_message_removed_language" 842 } 843 844 return locale.Tr(msgL10nKey, name, version) 845 } 846 847 func platformCommitMessage(op types.Operation, name, version string, word int) string { 848 var msgL10nKey string 849 switch op { 850 case types.OperationAdded: 851 msgL10nKey = "commit_message_added_platform" 852 case types.OperationUpdated: 853 msgL10nKey = "commit_message_updated_platform" 854 case types.OperationRemoved: 855 msgL10nKey = "commit_message_removed_platform" 856 } 857 858 return locale.Tr(msgL10nKey, name, strconv.Itoa(word), version) 859 } 860 861 func packageCommitMessage(op types.Operation, name, version string) string { 862 var msgL10nKey string 863 switch op { 864 case types.OperationAdded: 865 msgL10nKey = "commit_message_added_package" 866 case types.OperationUpdated: 867 msgL10nKey = "commit_message_updated_package" 868 case types.OperationRemoved: 869 msgL10nKey = "commit_message_removed_package" 870 } 871 872 if version == "" { 873 version = locale.Tl("package_version_auto", "auto") 874 } 875 return locale.Tr(msgL10nKey, name, version) 876 } 877 878 func commitMessageMultiple(requirements ...*Requirement) string { 879 var commitDetails []string 880 for _, req := range requirements { 881 commitDetails = append(commitDetails, requirementCommitMessage(req)) 882 } 883 884 return locale.Tl("commit_message_multiple", "Committing changes to multiple requirements: {{.V0}}", strings.Join(commitDetails, ", ")) 885 } 886 887 func requirementNames(requirements ...*Requirement) []string { 888 var names []string 889 for _, requirement := range requirements { 890 names = append(names, requirement.Name) 891 } 892 return names 893 }