github.com/jfrog/frogbot/v2@v2.21.0/utils/params.go (about)

     1  package utils
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"github.com/jfrog/jfrog-cli-security/commands/audit/jas"
     8  	"net/http"
     9  	"net/url"
    10  	"os"
    11  	"path/filepath"
    12  	"strconv"
    13  	"strings"
    14  
    15  	"github.com/jfrog/frogbot/v2/utils/outputwriter"
    16  	xrutils "github.com/jfrog/jfrog-cli-security/utils"
    17  
    18  	"github.com/jfrog/build-info-go/utils"
    19  	"github.com/jfrog/froggit-go/vcsclient"
    20  	"github.com/jfrog/froggit-go/vcsutils"
    21  	coreconfig "github.com/jfrog/jfrog-cli-core/v2/utils/config"
    22  	"github.com/jfrog/jfrog-client-go/utils/log"
    23  	"gopkg.in/yaml.v3"
    24  )
    25  
    26  const (
    27  	frogbotConfigDir  = ".frogbot"
    28  	FrogbotConfigFile = "frogbot-config.yml"
    29  )
    30  
    31  var (
    32  	errFrogbotConfigNotFound = fmt.Errorf("%s wasn't found in the Frogbot directory and its subdirectories. Assuming all the configuration is stored as environment variables", FrogbotConfigFile)
    33  	// Possible Config file path's to Frogbot Management repository
    34  	osFrogbotConfigPath = filepath.Join(frogbotConfigDir, FrogbotConfigFile)
    35  )
    36  
    37  type FrogbotDetails struct {
    38  	Repositories  RepoAggregator
    39  	ServerDetails *coreconfig.ServerDetails
    40  	GitClient     vcsclient.VcsClient
    41  	ReleasesRepo  string
    42  }
    43  
    44  type RepoAggregator []Repository
    45  
    46  // NewRepoAggregator returns an initialized RepoAggregator with an empty repository
    47  func newRepoAggregator() RepoAggregator {
    48  	return RepoAggregator{{Params: Params{Scan: Scan{Projects: []Project{{}}}}}}
    49  }
    50  
    51  type Repository struct {
    52  	Params       `yaml:"params,omitempty"`
    53  	OutputWriter outputwriter.OutputWriter
    54  	Server       coreconfig.ServerDetails
    55  }
    56  
    57  func (r *Repository) setOutputWriterDetails() {
    58  	r.OutputWriter = outputwriter.GetCompatibleOutputWriter(r.Params.GitProvider)
    59  	r.OutputWriter.SetAvoidExtraMessages(r.Params.AvoidExtraMessages)
    60  	r.OutputWriter.SetPullRequestCommentTitle(r.Params.PullRequestCommentTitle)
    61  }
    62  
    63  type Params struct {
    64  	Scan          `yaml:"scan,omitempty"`
    65  	Git           `yaml:"git,omitempty"`
    66  	JFrogPlatform `yaml:"jfrogPlatform,omitempty"`
    67  }
    68  
    69  func (p *Params) setDefaultsIfNeeded(gitParamsFromEnv *Git, commandName string) error {
    70  	if err := p.Git.setDefaultsIfNeeded(gitParamsFromEnv, commandName); err != nil {
    71  		return err
    72  	}
    73  	if err := p.JFrogPlatform.setDefaultsIfNeeded(); err != nil {
    74  		return err
    75  	}
    76  	return p.Scan.setDefaultsIfNeeded()
    77  }
    78  
    79  type Project struct {
    80  	InstallCommand      string   `yaml:"installCommand,omitempty"`
    81  	PipRequirementsFile string   `yaml:"pipRequirementsFile,omitempty"`
    82  	WorkingDirs         []string `yaml:"workingDirs,omitempty"`
    83  	PathExclusions      []string `yaml:"pathExclusions,omitempty"`
    84  	UseWrapper          *bool    `yaml:"useWrapper,omitempty"`
    85  	DepsRepo            string   `yaml:"repository,omitempty"`
    86  	InstallCommandName  string
    87  	InstallCommandArgs  []string
    88  	IsRecursiveScan     bool
    89  }
    90  
    91  func (p *Project) setDefaultsIfNeeded() error {
    92  	if len(p.WorkingDirs) == 0 {
    93  		workingDir := getTrimmedEnv(WorkingDirectoryEnv)
    94  		if workingDir == "" {
    95  
    96  			// If no working directories are provided, and none exist in the environment variable, we designate the project's root directory as our sole working directory.
    97  			// We then execute a recursive scan across the entire project, commencing from the root.
    98  			workingDir = RootDir
    99  			p.IsRecursiveScan = true
   100  		}
   101  		p.WorkingDirs = append(p.WorkingDirs, workingDir)
   102  	}
   103  	if len(p.PathExclusions) == 0 {
   104  		if p.PathExclusions, _ = readArrayParamFromEnv(PathExclusionsEnv, ";"); len(p.PathExclusions) == 0 {
   105  			p.PathExclusions = jas.DefaultExcludePatterns
   106  		}
   107  	}
   108  	if p.UseWrapper == nil {
   109  		useWrapper, err := getBoolEnv(UseWrapperEnv, true)
   110  		if err != nil {
   111  			return err
   112  		}
   113  		p.UseWrapper = &useWrapper
   114  	}
   115  	if p.InstallCommand == "" {
   116  		p.InstallCommand = getTrimmedEnv(InstallCommandEnv)
   117  	}
   118  	if p.InstallCommand != "" {
   119  		setProjectInstallCommand(p.InstallCommand, p)
   120  	}
   121  	if p.PipRequirementsFile == "" {
   122  		p.PipRequirementsFile = getTrimmedEnv(RequirementsFileEnv)
   123  	}
   124  	if p.DepsRepo == "" {
   125  		p.DepsRepo = getTrimmedEnv(DepsRepoEnv)
   126  	}
   127  	return nil
   128  }
   129  
   130  type Scan struct {
   131  	IncludeAllVulnerabilities       bool      `yaml:"includeAllVulnerabilities,omitempty"`
   132  	FixableOnly                     bool      `yaml:"fixableOnly,omitempty"`
   133  	FailOnSecurityIssues            *bool     `yaml:"failOnSecurityIssues,omitempty"`
   134  	AvoidPreviousPrCommentsDeletion bool      `yaml:"avoidPreviousPrCommentsDeletion,omitempty"`
   135  	MinSeverity                     string    `yaml:"minSeverity,omitempty"`
   136  	AllowedLicenses                 []string  `yaml:"allowedLicenses,omitempty"`
   137  	Projects                        []Project `yaml:"projects,omitempty"`
   138  	EmailDetails                    `yaml:",inline"`
   139  }
   140  
   141  type EmailDetails struct {
   142  	SmtpServer     string
   143  	SmtpPort       string
   144  	SmtpUser       string
   145  	SmtpPassword   string
   146  	EmailReceivers []string `yaml:"emailReceivers,omitempty"`
   147  }
   148  
   149  func (s *Scan) SetEmailDetails() error {
   150  	smtpServerAndPort := getTrimmedEnv(SmtpServerEnv)
   151  	if smtpServerAndPort == "" {
   152  		return nil
   153  	}
   154  	splittedServerAndPort := strings.Split(smtpServerAndPort, ":")
   155  	if len(splittedServerAndPort) < 2 {
   156  		return fmt.Errorf("failed while setting your email details. Could not extract the smtp server and its port from the %s environment variable. Expected format: `smtp.server.com:port`, received: %s", SmtpServerEnv, smtpServerAndPort)
   157  	}
   158  	s.SmtpServer = splittedServerAndPort[0]
   159  	s.SmtpPort = splittedServerAndPort[1]
   160  	s.SmtpUser = getTrimmedEnv(SmtpUserEnv)
   161  	s.SmtpPassword = getTrimmedEnv(SmtpPasswordEnv)
   162  	if s.SmtpUser == "" {
   163  		return fmt.Errorf("failed while setting your email details. SMTP username is expected, but the %s environment variable is empty", SmtpUserEnv)
   164  	}
   165  	if s.SmtpPassword == "" {
   166  		return fmt.Errorf("failed while setting your email details. SMTP password is expected, but the %s environment variable is empty", SmtpPasswordEnv)
   167  	}
   168  	if len(s.EmailReceivers) == 0 {
   169  		if emailReceiversEnv := getTrimmedEnv(EmailReceiversEnv); emailReceiversEnv != "" {
   170  			s.EmailReceivers = strings.Split(emailReceiversEnv, ",")
   171  		}
   172  	}
   173  	return nil
   174  }
   175  
   176  func (s *Scan) setDefaultsIfNeeded() (err error) {
   177  	e := &ErrMissingEnv{}
   178  	if !s.IncludeAllVulnerabilities {
   179  		if s.IncludeAllVulnerabilities, err = getBoolEnv(IncludeAllVulnerabilitiesEnv, false); err != nil {
   180  			return
   181  		}
   182  	}
   183  	if !s.AvoidPreviousPrCommentsDeletion {
   184  		if s.AvoidPreviousPrCommentsDeletion, err = getBoolEnv(AvoidPreviousPrCommentsDeletionEnv, false); err != nil {
   185  			return
   186  		}
   187  	}
   188  	if !s.FixableOnly {
   189  		if s.FixableOnly, err = getBoolEnv(FixableOnlyEnv, false); err != nil {
   190  			return
   191  		}
   192  	}
   193  	if s.FailOnSecurityIssues == nil {
   194  		var failOnSecurityIssues bool
   195  		if failOnSecurityIssues, err = getBoolEnv(FailOnSecurityIssuesEnv, true); err != nil {
   196  			return
   197  		}
   198  		s.FailOnSecurityIssues = &failOnSecurityIssues
   199  	}
   200  	if s.MinSeverity == "" {
   201  		if err = readParamFromEnv(MinSeverityEnv, &s.MinSeverity); err != nil && !e.IsMissingEnvErr(err) {
   202  			return
   203  		}
   204  	}
   205  	if s.MinSeverity, err = xrutils.GetSeveritiesFormat(s.MinSeverity); err != nil {
   206  		return
   207  	}
   208  	if len(s.Projects) == 0 {
   209  		s.Projects = append(s.Projects, Project{})
   210  	}
   211  	if len(s.AllowedLicenses) == 0 {
   212  		if s.AllowedLicenses, err = readArrayParamFromEnv(AllowedLicensesEnv, ","); err != nil && !e.IsMissingEnvErr(err) {
   213  			return
   214  		}
   215  	}
   216  	for i := range s.Projects {
   217  		if err = s.Projects[i].setDefaultsIfNeeded(); err != nil {
   218  			return
   219  		}
   220  	}
   221  	err = s.SetEmailDetails()
   222  	return
   223  }
   224  
   225  type JFrogPlatform struct {
   226  	Watches         []string `yaml:"watches,omitempty"`
   227  	JFrogProjectKey string   `yaml:"jfrogProjectKey,omitempty"`
   228  }
   229  
   230  func (jp *JFrogPlatform) setDefaultsIfNeeded() (err error) {
   231  	e := &ErrMissingEnv{}
   232  	if len(jp.Watches) == 0 {
   233  		if jp.Watches, err = readArrayParamFromEnv(jfrogWatchesEnv, WatchesDelimiter); err != nil && !e.IsMissingEnvErr(err) {
   234  			return
   235  		}
   236  	}
   237  
   238  	if jp.JFrogProjectKey == "" {
   239  		if err = readParamFromEnv(jfrogProjectEnv, &jp.JFrogProjectKey); err != nil && !e.IsMissingEnvErr(err) {
   240  			return
   241  		}
   242  		// We don't want to return an error from this function if the error is of type ErrMissingEnv because JFrogPlatform environment variables are not mandatory.
   243  		err = nil
   244  	}
   245  	return
   246  }
   247  
   248  type Git struct {
   249  	GitProvider vcsutils.VcsProvider
   250  	vcsclient.VcsInfo
   251  	RepoOwner                string
   252  	RepoName                 string   `yaml:"repoName,omitempty"`
   253  	Branches                 []string `yaml:"branches,omitempty"`
   254  	BranchNameTemplate       string   `yaml:"branchNameTemplate,omitempty"`
   255  	CommitMessageTemplate    string   `yaml:"commitMessageTemplate,omitempty"`
   256  	PullRequestTitleTemplate string   `yaml:"pullRequestTitleTemplate,omitempty"`
   257  	PullRequestCommentTitle  string   `yaml:"pullRequestCommentTitle,omitempty"`
   258  	AvoidExtraMessages       bool     `yaml:"avoidExtraMessages,omitempty"`
   259  	EmailAuthor              string   `yaml:"emailAuthor,omitempty"`
   260  	AggregateFixes           bool     `yaml:"aggregateFixes,omitempty"`
   261  	PullRequestDetails       vcsclient.PullRequestInfo
   262  	RepositoryCloneUrl       string
   263  }
   264  
   265  func (g *Git) setDefaultsIfNeeded(gitParamsFromEnv *Git, commandName string) (err error) {
   266  	g.RepoOwner = gitParamsFromEnv.RepoOwner
   267  	g.GitProvider = gitParamsFromEnv.GitProvider
   268  	g.VcsInfo = gitParamsFromEnv.VcsInfo
   269  	g.PullRequestDetails = gitParamsFromEnv.PullRequestDetails
   270  	if g.RepoName == "" {
   271  		if gitParamsFromEnv.RepoName == "" {
   272  			return fmt.Errorf("repository name is missing. please set the repository name in your %s file or as the %s environment variable", FrogbotConfigFile, GitRepoEnv)
   273  		}
   274  		g.RepoName = gitParamsFromEnv.RepoName
   275  	}
   276  	if g.EmailAuthor == "" {
   277  		if g.EmailAuthor = getTrimmedEnv(GitEmailAuthorEnv); g.EmailAuthor == "" {
   278  			g.EmailAuthor = frogbotAuthorEmail
   279  		}
   280  	}
   281  	if commandName == ScanPullRequest {
   282  		if err = g.extractScanPullRequestEnvParams(gitParamsFromEnv); err != nil {
   283  			return
   284  		}
   285  	}
   286  	if commandName == ScanRepository || commandName == ScanMultipleRepositories {
   287  		if err = g.extractScanRepositoryEnvParams(gitParamsFromEnv); err != nil {
   288  			return
   289  		}
   290  	}
   291  	return
   292  }
   293  
   294  func (g *Git) extractScanPullRequestEnvParams(gitParamsFromEnv *Git) (err error) {
   295  	// The Pull Request ID is a mandatory requirement for Frogbot to properly identify and scan the relevant pull request
   296  	if gitParamsFromEnv.PullRequestDetails.ID == 0 {
   297  		return errors.New("no Pull Request ID has been provided. Please configure it by using the `JF_GIT_PULL_REQUEST_ID` environment variable")
   298  	}
   299  	if g.PullRequestCommentTitle == "" {
   300  		g.PullRequestCommentTitle = getTrimmedEnv(PullRequestCommentTitleEnv)
   301  	}
   302  	g.AvoidExtraMessages, err = getBoolEnv(AvoidExtraMessages, false)
   303  	return
   304  }
   305  
   306  func (g *Git) extractScanRepositoryEnvParams(gitParamsFromEnv *Git) (err error) {
   307  	// Continue to extract ScanRepository related env params
   308  	noBranchesProvidedViaConfig := len(g.Branches) == 0
   309  	noBranchesProvidedViaEnv := len(gitParamsFromEnv.Branches) == 0
   310  	if noBranchesProvidedViaConfig {
   311  		if noBranchesProvidedViaEnv {
   312  			return errors.New("no branches were provided. Please set your branches using the `JF_GIT_BASE_BRANCH` environment variable or by configuring them in the frogbot-config.yml file")
   313  		}
   314  		g.Branches = gitParamsFromEnv.Branches
   315  	}
   316  	if g.BranchNameTemplate == "" {
   317  		branchTemplate := getTrimmedEnv(BranchNameTemplateEnv)
   318  		if err = validateHashPlaceHolder(branchTemplate); err != nil {
   319  			return
   320  		}
   321  		g.BranchNameTemplate = branchTemplate
   322  	}
   323  	if g.CommitMessageTemplate == "" {
   324  		g.CommitMessageTemplate = getTrimmedEnv(CommitMessageTemplateEnv)
   325  	}
   326  	if g.PullRequestTitleTemplate == "" {
   327  		g.PullRequestTitleTemplate = getTrimmedEnv(PullRequestTitleTemplateEnv)
   328  	}
   329  	if !g.AggregateFixes {
   330  		if g.AggregateFixes, err = getBoolEnv(GitAggregateFixesEnv, false); err != nil {
   331  			return
   332  		}
   333  	}
   334  	return
   335  }
   336  
   337  func validateHashPlaceHolder(template string) error {
   338  	if template == "" {
   339  		return nil
   340  	}
   341  	if !strings.Contains(template, BranchHashPlaceHolder) {
   342  		return fmt.Errorf("branch name template must contain %s, provided: %s", BranchHashPlaceHolder, template)
   343  	}
   344  	return nil
   345  }
   346  
   347  func GetFrogbotDetails(commandName string) (frogbotDetails *FrogbotDetails, err error) {
   348  	// Get server and git details
   349  	jfrogServer, err := extractJFrogCredentialsFromEnvs()
   350  	if err != nil {
   351  		return
   352  	}
   353  	gitParamsFromEnv, err := extractGitParamsFromEnvs(commandName)
   354  	if err != nil {
   355  		return
   356  	}
   357  
   358  	defer func() {
   359  		err = errors.Join(err, SanitizeEnv())
   360  	}()
   361  
   362  	// Build a version control client for REST API requests
   363  	client, err := vcsclient.
   364  		NewClientBuilder(gitParamsFromEnv.GitProvider).
   365  		ApiEndpoint(strings.TrimSuffix(gitParamsFromEnv.APIEndpoint, "/")).
   366  		Token(gitParamsFromEnv.Token).
   367  		Project(gitParamsFromEnv.Project).
   368  		Logger(log.GetLogger()).
   369  		Username(gitParamsFromEnv.Username).
   370  		Build()
   371  	if err != nil {
   372  		return
   373  	}
   374  
   375  	configAggregator, err := getConfigAggregator(client, gitParamsFromEnv, jfrogServer, commandName)
   376  	if err != nil {
   377  		return
   378  	}
   379  
   380  	frogbotDetails = &FrogbotDetails{Repositories: configAggregator, GitClient: client, ServerDetails: jfrogServer, ReleasesRepo: os.Getenv(jfrogReleasesRepoEnv)}
   381  	return
   382  }
   383  
   384  // getConfigAggregator returns a RepoAggregator based on frogbot-config.yml and environment variables.
   385  func getConfigAggregator(gitClient vcsclient.VcsClient, gitParamsFromEnv *Git, jfrogServer *coreconfig.ServerDetails, commandName string) (RepoAggregator, error) {
   386  	configFileContent, err := getConfigFileContent(gitClient, gitParamsFromEnv, commandName)
   387  	if err != nil {
   388  		return nil, err
   389  	}
   390  	if configFileContent != nil {
   391  		log.Debug(fmt.Sprintf("The content of %s that will be used is:\n%s", FrogbotConfigFile, string(configFileContent)))
   392  	}
   393  	return BuildRepoAggregator(gitClient, configFileContent, gitParamsFromEnv, jfrogServer, commandName)
   394  }
   395  
   396  // getConfigFileContent retrieves the content of the frogbot-config.yml file
   397  func getConfigFileContent(gitClient vcsclient.VcsClient, gitParamsFromEnv *Git, commandName string) ([]byte, error) {
   398  	var errMissingConfig *ErrMissingConfig
   399  
   400  	if commandName == ScanRepository || commandName == ScanMultipleRepositories {
   401  		configFileContent, err := ReadConfigFromFileSystem(osFrogbotConfigPath)
   402  		if err != nil && !errors.As(err, &errMissingConfig) {
   403  			return nil, err
   404  		}
   405  		if configFileContent != nil {
   406  			return configFileContent, nil
   407  		}
   408  	}
   409  
   410  	configFileContent, err := readConfigFromTarget(gitClient, gitParamsFromEnv)
   411  	if errors.As(err, &errMissingConfig) {
   412  		// Avoid returning an error if the frogbot-config.yml file is missing.
   413  		// If an error occurs because the file is missing, we will create an environment variable-based configuration aggregator instead.
   414  		return nil, nil
   415  	}
   416  	return configFileContent, err
   417  }
   418  
   419  // BuildRepoAggregator receives the content of a frogbot-config.yml file, along with the Git (built from environment variables) and ServerDetails parameters.
   420  // Returns a RepoAggregator instance with all the defaults and necessary fields.
   421  func BuildRepoAggregator(gitClient vcsclient.VcsClient, configFileContent []byte, gitParamsFromEnv *Git, server *coreconfig.ServerDetails, commandName string) (resultAggregator RepoAggregator, err error) {
   422  	var cleanAggregator RepoAggregator
   423  	// Unmarshal the frogbot-config.yml file if exists
   424  	if cleanAggregator, err = unmarshalFrogbotConfigYaml(configFileContent); err != nil {
   425  		return
   426  	}
   427  	for _, repository := range cleanAggregator {
   428  		repository.Server = *server
   429  		if err = repository.Params.setDefaultsIfNeeded(gitParamsFromEnv, commandName); err != nil {
   430  			return
   431  		}
   432  		repository.setOutputWriterDetails()
   433  		repository.OutputWriter.SetSizeLimit(gitClient)
   434  		resultAggregator = append(resultAggregator, repository)
   435  	}
   436  
   437  	return
   438  }
   439  
   440  // unmarshalFrogbotConfigYaml uses the yaml.Unmarshaler interface to parse the yamlContent.
   441  // If there is no config file, the function returns a RepoAggregator with an empty repository.
   442  func unmarshalFrogbotConfigYaml(yamlContent []byte) (result RepoAggregator, err error) {
   443  	if len(yamlContent) == 0 {
   444  		result = newRepoAggregator()
   445  		return
   446  	}
   447  	err = yaml.Unmarshal(yamlContent, &result)
   448  	return
   449  }
   450  
   451  func extractJFrogCredentialsFromEnvs() (*coreconfig.ServerDetails, error) {
   452  	server := coreconfig.ServerDetails{}
   453  	platformUrl := strings.TrimSuffix(getTrimmedEnv(JFrogUrlEnv), "/")
   454  	xrUrl := strings.TrimSuffix(getTrimmedEnv(jfrogXrayUrlEnv), "/")
   455  	rtUrl := strings.TrimSuffix(getTrimmedEnv(jfrogArtifactoryUrlEnv), "/")
   456  	if xrUrl != "" && rtUrl != "" {
   457  		server.XrayUrl = xrUrl + "/"
   458  		server.ArtifactoryUrl = rtUrl + "/"
   459  	} else {
   460  		if platformUrl == "" {
   461  			return nil, fmt.Errorf("%s or %s and %s environment variables are missing", JFrogUrlEnv, jfrogXrayUrlEnv, jfrogArtifactoryUrlEnv)
   462  		}
   463  		server.Url = platformUrl + "/"
   464  		server.XrayUrl = platformUrl + "/xray/"
   465  		server.ArtifactoryUrl = platformUrl + "/artifactory/"
   466  	}
   467  
   468  	password := getTrimmedEnv(JFrogPasswordEnv)
   469  	user := getTrimmedEnv(JFrogUserEnv)
   470  	if password != "" && user != "" {
   471  		server.User = user
   472  		server.Password = password
   473  	} else if accessToken := getTrimmedEnv(JFrogTokenEnv); accessToken != "" {
   474  		server.AccessToken = accessToken
   475  	} else {
   476  		return nil, fmt.Errorf("%s and %s or %s environment variables are missing", JFrogUserEnv, JFrogPasswordEnv, JFrogTokenEnv)
   477  	}
   478  	return &server, nil
   479  }
   480  
   481  func extractGitParamsFromEnvs(commandName string) (*Git, error) {
   482  	e := &ErrMissingEnv{}
   483  	var err error
   484  	gitEnvParams := &Git{}
   485  	// Branch & Repo names are mandatory variables.
   486  	// Must be set in the frogbot-config.yml or as an environment variables.
   487  	// Validation performed later
   488  	// Set the base branch name
   489  	var branch string
   490  	if err = readParamFromEnv(GitBaseBranchEnv, &branch); err != nil && !e.IsMissingEnvErr(err) {
   491  		return nil, err
   492  	}
   493  	if branch != "" {
   494  		gitEnvParams.Branches = []string{branch}
   495  	}
   496  	// Non-mandatory Git Api Endpoint, if not set, default values will be used.
   497  	if err = readParamFromEnv(GitApiEndpointEnv, &gitEnvParams.APIEndpoint); err != nil && !e.IsMissingEnvErr(err) {
   498  		return nil, err
   499  	}
   500  	if err = verifyValidApiEndpoint(gitEnvParams.APIEndpoint); err != nil {
   501  		return nil, err
   502  	}
   503  	// [Mandatory] Set the Git provider
   504  	if gitEnvParams.GitProvider, err = extractVcsProviderFromEnv(); err != nil {
   505  		return nil, err
   506  	}
   507  	// [Mandatory] Set the git repository owner name (organization)
   508  	if err = readParamFromEnv(GitRepoOwnerEnv, &gitEnvParams.RepoOwner); err != nil {
   509  		return nil, err
   510  	}
   511  	// [Mandatory] Set the access token to the git provider
   512  	if err = readParamFromEnv(GitTokenEnv, &gitEnvParams.Token); err != nil {
   513  		return nil, err
   514  	}
   515  
   516  	// [Mandatory] Set the repository name, except for multi repository.
   517  	if err = readParamFromEnv(GitRepoEnv, &gitEnvParams.RepoName); err != nil && commandName != ScanMultipleRepositories {
   518  		return nil, err
   519  	}
   520  
   521  	// Set Bitbucket Server username
   522  	// Mandatory only for Bitbucket Server, this authentication detail is required for performing git operations.
   523  	if err = readParamFromEnv(GitUsernameEnv, &gitEnvParams.Username); err != nil && !e.IsMissingEnvErr(err) {
   524  		return nil, err
   525  	}
   526  	// Set Azure Repos Project name
   527  	// Mandatory for Azure Repos only
   528  	if err = readParamFromEnv(GitProjectEnv, &gitEnvParams.Project); err != nil && gitEnvParams.GitProvider == vcsutils.AzureRepos {
   529  		return nil, err
   530  	}
   531  	if envPrId := getTrimmedEnv(GitPullRequestIDEnv); envPrId != "" {
   532  		var convertedPrId int
   533  		if convertedPrId, err = strconv.Atoi(envPrId); err != nil {
   534  			return nil, fmt.Errorf("failed parsing %s environment variable as a number. The received environment is : %s", GitPullRequestIDEnv, envPrId)
   535  		}
   536  		gitEnvParams.PullRequestDetails = vcsclient.PullRequestInfo{ID: int64(convertedPrId)}
   537  	}
   538  
   539  	return gitEnvParams, nil
   540  }
   541  
   542  func verifyValidApiEndpoint(apiEndpoint string) error {
   543  	// Empty string will resolve to default values.
   544  	if apiEndpoint == "" {
   545  		return nil
   546  	}
   547  	parsedUrl, err := url.Parse(apiEndpoint)
   548  	if err != nil {
   549  		return err
   550  	}
   551  	if parsedUrl.Scheme == "" {
   552  		return errors.New("the given API endpoint is invalid. Please note that the API endpoint format should be provided with the 'HTTPS' protocol as a prefix")
   553  	}
   554  	return nil
   555  }
   556  
   557  func readArrayParamFromEnv(envKey, delimiter string) ([]string, error) {
   558  	var envValue string
   559  	var err error
   560  	e := &ErrMissingEnv{}
   561  	if err = readParamFromEnv(envKey, &envValue); err != nil && !e.IsMissingEnvErr(err) {
   562  		return nil, err
   563  	}
   564  	if envValue == "" {
   565  		return nil, &ErrMissingEnv{VariableName: envKey}
   566  	}
   567  	// Remove spaces if exists
   568  	envValue = strings.ReplaceAll(envValue, " ", "")
   569  	return strings.Split(envValue, delimiter), nil
   570  }
   571  
   572  func readParamFromEnv(envKey string, paramValue *string) error {
   573  	*paramValue = getTrimmedEnv(envKey)
   574  	if *paramValue == "" {
   575  		return &ErrMissingEnv{VariableName: envKey}
   576  	}
   577  	return nil
   578  }
   579  
   580  func getTrimmedEnv(envKey string) string {
   581  	return strings.TrimSpace(os.Getenv(envKey))
   582  }
   583  
   584  func extractVcsProviderFromEnv() (vcsutils.VcsProvider, error) {
   585  	vcsProvider := getTrimmedEnv(GitProvider)
   586  	switch vcsProvider {
   587  	case string(GitHub):
   588  		return vcsutils.GitHub, nil
   589  	case string(GitLab):
   590  		return vcsutils.GitLab, nil
   591  	// For backward compatibility, we are accepting also "bitbucket server"
   592  	case string(BitbucketServer), "bitbucket server":
   593  		return vcsutils.BitbucketServer, nil
   594  	case string(AzureRepos):
   595  		return vcsutils.AzureRepos, nil
   596  	}
   597  	return 0, fmt.Errorf("%s should be one of: '%s', '%s', '%s' or '%s'", GitProvider, GitHub, GitLab, BitbucketServer, AzureRepos)
   598  }
   599  
   600  func SanitizeEnv() error {
   601  	for _, env := range os.Environ() {
   602  		if !strings.HasPrefix(env, "JF_") {
   603  			continue
   604  		}
   605  		envSplit := strings.Split(env, "=")
   606  		if err := os.Unsetenv(envSplit[0]); err != nil {
   607  			return err
   608  		}
   609  	}
   610  	return nil
   611  }
   612  
   613  // ReadConfigFromFileSystem looks for .frogbot/frogbot-config.yml from the given path and return its content. The path is relative and starts from the root of the project.
   614  // If the config file is not found in the relative path, it will search in parent dirs.
   615  func ReadConfigFromFileSystem(configRelativePath string) (configFileContent []byte, err error) {
   616  	log.Debug("Reading config from file system. Looking for", osFrogbotConfigPath)
   617  	fullConfigDirPath, err := filepath.Abs(configRelativePath)
   618  	if err != nil {
   619  		return nil, err
   620  	}
   621  
   622  	// Look for the frogbot-config.yml file in fullConfigPath
   623  	exist, err := utils.IsFileExists(fullConfigDirPath, false)
   624  	if !exist || err != nil {
   625  		// Look for the frogbot-config.yml in fullConfigPath parents dirs
   626  		log.Debug(FrogbotConfigFile, "wasn't found in "+fullConfigDirPath+". Searching for it in upstream directories")
   627  		if fullConfigDirPath, err = utils.FindFileInDirAndParents(fullConfigDirPath, configRelativePath); err != nil {
   628  			return nil, &ErrMissingConfig{errFrogbotConfigNotFound.Error()}
   629  		}
   630  		fullConfigDirPath = filepath.Join(fullConfigDirPath, configRelativePath)
   631  	}
   632  
   633  	log.Debug(FrogbotConfigFile, "found in", fullConfigDirPath)
   634  	configFileContent, err = os.ReadFile(filepath.Clean(fullConfigDirPath))
   635  	if err != nil {
   636  		err = fmt.Errorf("an error occurd while reading the %s file at: %s\n%s", FrogbotConfigFile, configRelativePath, err.Error())
   637  	}
   638  	return
   639  }
   640  
   641  func setProjectInstallCommand(installCommand string, project *Project) {
   642  	parts := strings.Fields(installCommand)
   643  	if len(parts) > 1 {
   644  		project.InstallCommandArgs = parts[1:]
   645  	}
   646  	project.InstallCommandName = parts[0]
   647  }
   648  
   649  func getBoolEnv(envKey string, defaultValue bool) (bool, error) {
   650  	envValue := getTrimmedEnv(envKey)
   651  	if envValue != "" {
   652  		parsedEnv, err := strconv.ParseBool(envValue)
   653  		if err != nil {
   654  			return false, fmt.Errorf("the value of the %s environment is expected to be either TRUE or FALSE. The value received however is %s", envKey, envValue)
   655  		}
   656  		return parsedEnv, nil
   657  	}
   658  
   659  	return defaultValue, nil
   660  }
   661  
   662  // readConfigFromTarget reads the .frogbot/frogbot-config.yml from the target repository
   663  func readConfigFromTarget(client vcsclient.VcsClient, gitParamsFromEnv *Git) (configContent []byte, err error) {
   664  	// Extract repository details from Git parameters
   665  	repoName := gitParamsFromEnv.RepoName
   666  	repoOwner := gitParamsFromEnv.RepoOwner
   667  	branches := gitParamsFromEnv.Branches
   668  
   669  	if repoName == "" && repoOwner == "" {
   670  		return
   671  	}
   672  
   673  	log.Debug("Attempting to download", FrogbotConfigFile, "from", repoOwner+"/"+repoName)
   674  
   675  	var branch string
   676  	if len(branches) == 0 {
   677  		log.Debug(GitBaseBranchEnv, "is missing. Assuming that the", FrogbotConfigFile, "file exists on default branch")
   678  	} else {
   679  		// We encounter this scenario when the JF_GIT_BASE_BRANCH is defined. In this situation, we have only one branch.
   680  		branch = branches[0]
   681  		log.Debug("The", FrogbotConfigFile, "will be downloaded from", branch, "branch")
   682  	}
   683  
   684  	// Construct the path to the frogbot-config.yml file in the repository
   685  	gitFrogbotConfigPath := fmt.Sprintf("%s/%s", frogbotConfigDir, FrogbotConfigFile)
   686  
   687  	// Download the frogbot-config.yml file from the repository
   688  	var statusCode int
   689  	configContent, statusCode, err = client.DownloadFileFromRepo(context.Background(), repoOwner, repoName, branch, gitFrogbotConfigPath)
   690  
   691  	// Handle different HTTP status codes
   692  	switch statusCode {
   693  	case http.StatusOK:
   694  		log.Info(fmt.Sprintf("Successfully downloaded %s file from <%s/%s/%s>", FrogbotConfigFile, repoOwner, repoName, branch))
   695  	case http.StatusNotFound:
   696  		log.Debug(fmt.Sprintf("The %s file wasn't recognized in <%s/%s>", gitFrogbotConfigPath, repoOwner, repoName))
   697  		// If .frogbot/frogbot-config.yml isn't found, return an ErrMissingConfig
   698  		configContent = nil
   699  		err = &ErrMissingConfig{errFrogbotConfigNotFound.Error()}
   700  	case http.StatusUnauthorized:
   701  		log.Warn("Your credentials seem to be invalid. If you are using an on-premises Git provider, please set the API endpoint of your Git provider using the 'JF_GIT_API_ENDPOINT' environment variable (example: 'https://gitlab.example.com'). Additionally, make sure that the provided credentials have the required Git permissions.")
   702  	}
   703  	return
   704  }