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  }