github.com/jfrog/jfrog-cli-core/v2@v2.51.0/utils/coreutils/techutils.go (about)

     1  package coreutils
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"strings"
     8  
     9  	"github.com/jfrog/gofrog/datastructures"
    10  	"github.com/jfrog/jfrog-client-go/artifactory/services/fspatterns"
    11  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    12  	"github.com/jfrog/jfrog-client-go/utils/log"
    13  
    14  	"golang.org/x/exp/maps"
    15  	"golang.org/x/text/cases"
    16  	"golang.org/x/text/language"
    17  
    18  	"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
    19  )
    20  
    21  type Technology string
    22  
    23  const (
    24  	Maven  Technology = "maven"
    25  	Gradle Technology = "gradle"
    26  	Npm    Technology = "npm"
    27  	Pnpm   Technology = "pnpm"
    28  	Yarn   Technology = "yarn"
    29  	Go     Technology = "go"
    30  	Pip    Technology = "pip"
    31  	Pipenv Technology = "pipenv"
    32  	Poetry Technology = "poetry"
    33  	Nuget  Technology = "nuget"
    34  	Dotnet Technology = "dotnet"
    35  	Docker Technology = "docker"
    36  )
    37  
    38  const Pypi = "pypi"
    39  
    40  type TechData struct {
    41  	// The name of the package type used in this technology.
    42  	packageType string
    43  	// Suffixes of file/directory names that indicate if a project uses this technology.
    44  	// The name of at least one of the files/directories in the project's directory must end with one of these suffixes.
    45  	indicators []string
    46  	// Suffixes of file/directory names that indicate if a project does not use this technology.
    47  	// The names of all the files/directories in the project's directory must NOT end with any of these suffixes.
    48  	exclude []string
    49  	// Whether this technology is supported by the 'jf ci-setup' command.
    50  	ciSetupSupport bool
    51  	// Whether Contextual Analysis supported in this technology.
    52  	applicabilityScannable bool
    53  	// The files that handle the project's dependencies.
    54  	packageDescriptors []string
    55  	// Formal name of the technology
    56  	formal string
    57  	// The executable name of the technology
    58  	execCommand string
    59  	// The operator for package versioning
    60  	packageVersionOperator string
    61  	// The package installation command of a package
    62  	packageInstallationCommand string
    63  }
    64  
    65  var technologiesData = map[Technology]TechData{
    66  	Maven: {
    67  		indicators:             []string{"pom.xml"},
    68  		ciSetupSupport:         true,
    69  		packageDescriptors:     []string{"pom.xml"},
    70  		execCommand:            "mvn",
    71  		applicabilityScannable: true,
    72  	},
    73  	Gradle: {
    74  		indicators:             []string{"build.gradle", "build.gradle.kts"},
    75  		ciSetupSupport:         true,
    76  		packageDescriptors:     []string{"build.gradle", "build.gradle.kts"},
    77  		applicabilityScannable: true,
    78  	},
    79  	Npm: {
    80  		indicators:                 []string{"package.json", "package-lock.json", "npm-shrinkwrap.json"},
    81  		exclude:                    []string{"pnpm-lock.yaml", ".yarnrc.yml", "yarn.lock", ".yarn"},
    82  		ciSetupSupport:             true,
    83  		packageDescriptors:         []string{"package.json"},
    84  		formal:                     string(Npm),
    85  		packageVersionOperator:     "@",
    86  		packageInstallationCommand: "install",
    87  		applicabilityScannable:     true,
    88  	},
    89  	Pnpm: {
    90  		indicators:                 []string{"pnpm-lock.yaml"},
    91  		exclude:                    []string{".yarnrc.yml", "yarn.lock", ".yarn"},
    92  		packageDescriptors:         []string{"package.json"},
    93  		packageVersionOperator:     "@",
    94  		packageInstallationCommand: "update",
    95  		applicabilityScannable:     true,
    96  	},
    97  	Yarn: {
    98  		indicators:             []string{".yarnrc.yml", "yarn.lock", ".yarn", ".yarnrc"},
    99  		exclude:                []string{"pnpm-lock.yaml"},
   100  		packageDescriptors:     []string{"package.json"},
   101  		packageVersionOperator: "@",
   102  		applicabilityScannable: true,
   103  	},
   104  	Go: {
   105  		indicators:                 []string{"go.mod"},
   106  		packageDescriptors:         []string{"go.mod"},
   107  		packageVersionOperator:     "@v",
   108  		packageInstallationCommand: "get",
   109  	},
   110  	Pip: {
   111  		packageType:            Pypi,
   112  		indicators:             []string{"setup.py", "requirements.txt"},
   113  		packageDescriptors:     []string{"setup.py", "requirements.txt"},
   114  		exclude:                []string{"Pipfile", "Pipfile.lock", "pyproject.toml", "poetry.lock"},
   115  		applicabilityScannable: true,
   116  	},
   117  	Pipenv: {
   118  		packageType:                Pypi,
   119  		indicators:                 []string{"Pipfile", "Pipfile.lock"},
   120  		packageDescriptors:         []string{"Pipfile"},
   121  		packageVersionOperator:     "==",
   122  		packageInstallationCommand: "install",
   123  		applicabilityScannable:     true,
   124  	},
   125  	Poetry: {
   126  		packageType:                Pypi,
   127  		indicators:                 []string{"pyproject.toml", "poetry.lock"},
   128  		packageDescriptors:         []string{"pyproject.toml"},
   129  		packageInstallationCommand: "add",
   130  		packageVersionOperator:     "==",
   131  		applicabilityScannable:     true,
   132  	},
   133  	Nuget: {
   134  		indicators:         []string{".sln", ".csproj"},
   135  		packageDescriptors: []string{".sln", ".csproj"},
   136  		formal:             "NuGet",
   137  		// .NET CLI is used for NuGet projects
   138  		execCommand:                "dotnet",
   139  		packageInstallationCommand: "add",
   140  		// packageName -v packageVersion
   141  		packageVersionOperator: " -v ",
   142  	},
   143  	Dotnet: {
   144  		indicators:         []string{".sln", ".csproj"},
   145  		packageDescriptors: []string{".sln", ".csproj"},
   146  		formal:             ".NET",
   147  	},
   148  }
   149  
   150  func (tech Technology) ToFormal() string {
   151  	if technologiesData[tech].formal == "" {
   152  		return cases.Title(language.Und).String(tech.String())
   153  	}
   154  	return technologiesData[tech].formal
   155  }
   156  
   157  func (tech Technology) String() string {
   158  	return string(tech)
   159  }
   160  
   161  func (tech Technology) GetExecCommandName() string {
   162  	if technologiesData[tech].execCommand == "" {
   163  		return tech.String()
   164  	}
   165  	return technologiesData[tech].execCommand
   166  }
   167  
   168  func (tech Technology) GetPackageType() string {
   169  	if technologiesData[tech].packageType == "" {
   170  		return tech.String()
   171  	}
   172  	return technologiesData[tech].packageType
   173  }
   174  
   175  func (tech Technology) GetPackageDescriptor() []string {
   176  	return technologiesData[tech].packageDescriptors
   177  }
   178  
   179  func (tech Technology) IsCiSetup() bool {
   180  	return technologiesData[tech].ciSetupSupport
   181  }
   182  
   183  func (tech Technology) GetPackageVersionOperator() string {
   184  	return technologiesData[tech].packageVersionOperator
   185  }
   186  
   187  func (tech Technology) GetPackageInstallationCommand() string {
   188  	return technologiesData[tech].packageInstallationCommand
   189  }
   190  
   191  func (tech Technology) ApplicabilityScannable() bool {
   192  	return technologiesData[tech].applicabilityScannable
   193  }
   194  
   195  func DetectedTechnologiesList() (technologies []string) {
   196  	wd, err := os.Getwd()
   197  	if errorutils.CheckError(err) != nil {
   198  		return
   199  	}
   200  	return detectedTechnologiesListInPath(wd, false)
   201  }
   202  
   203  func detectedTechnologiesListInPath(path string, recursive bool) (technologies []string) {
   204  	detectedTechnologies, err := DetectTechnologies(path, false, recursive)
   205  	if err != nil {
   206  		return
   207  	}
   208  	if len(detectedTechnologies) == 0 {
   209  		return
   210  	}
   211  	techStringsList := DetectedTechnologiesToSlice(detectedTechnologies)
   212  	log.Info(fmt.Sprintf("Detected: %s.", strings.Join(techStringsList, ", ")))
   213  	return techStringsList
   214  }
   215  
   216  // If recursive is true, the search will not be limited to files in the root path.
   217  // If requestedTechs is empty, all technologies will be checked.
   218  // If excludePathPattern is not empty, files/directories that match the wildcard pattern will be excluded from the search.
   219  func DetectTechnologiesDescriptors(path string, recursive bool, requestedTechs []string, requestedDescriptors map[Technology][]string, excludePathPattern string) (technologiesDetected map[Technology]map[string][]string, err error) {
   220  	filesList, err := fspatterns.ListFiles(path, recursive, false, true, true, excludePathPattern)
   221  	if err != nil {
   222  		return
   223  	}
   224  	workingDirectoryToIndicators, excludedTechAtWorkingDir := mapFilesToRelevantWorkingDirectories(filesList, requestedDescriptors)
   225  	var strJson string
   226  	if strJson, err = GetJsonIndent(workingDirectoryToIndicators); err != nil {
   227  		return
   228  	} else if len(workingDirectoryToIndicators) > 0 {
   229  		log.Debug(fmt.Sprintf("mapped %d working directories with indicators/descriptors:\n%s", len(workingDirectoryToIndicators), strJson))
   230  	}
   231  	technologiesDetected = mapWorkingDirectoriesToTechnologies(workingDirectoryToIndicators, excludedTechAtWorkingDir, ToTechnologies(requestedTechs), requestedDescriptors)
   232  	if len(technologiesDetected) > 0 {
   233  		log.Debug(fmt.Sprintf("Detected %d technologies at %s: %s.", len(technologiesDetected), path, maps.Keys(technologiesDetected)))
   234  	}
   235  	return
   236  }
   237  
   238  // Map files to relevant working directories according to the technologies' indicators/descriptors and requested descriptors.
   239  // files: The file paths to map.
   240  // requestedDescriptors: Special requested descriptors (for example in Pip requirement.txt can have different path) for each technology.
   241  // Returns:
   242  //  1. workingDirectoryToIndicators: A map of working directories to the files that are relevant to the technologies.
   243  //     wd1: [wd1/indicator, wd1/descriptor]
   244  //     wd/wd2: [wd/wd2/indicator]
   245  //  2. excludedTechAtWorkingDir: A map of working directories to the technologies that are excluded from the working directory.
   246  //     wd1: [tech1, tech2]
   247  //     wd/wd2: [tech1]
   248  func mapFilesToRelevantWorkingDirectories(files []string, requestedDescriptors map[Technology][]string) (workingDirectoryToIndicators map[string][]string, excludedTechAtWorkingDir map[string][]Technology) {
   249  	workingDirectoryToIndicatorsSet := make(map[string]*datastructures.Set[string])
   250  	excludedTechAtWorkingDir = make(map[string][]Technology)
   251  	for _, path := range files {
   252  		directory := filepath.Dir(path)
   253  
   254  		for tech, techData := range technologiesData {
   255  			// Check if the working directory contains indicators/descriptors for the technology
   256  			relevant := isIndicator(path, techData) || isDescriptor(path, techData) || isRequestedDescriptor(path, requestedDescriptors[tech])
   257  			if relevant {
   258  				if _, exist := workingDirectoryToIndicatorsSet[directory]; !exist {
   259  					workingDirectoryToIndicatorsSet[directory] = datastructures.MakeSet[string]()
   260  				}
   261  				workingDirectoryToIndicatorsSet[directory].Add(path)
   262  			}
   263  			// Check if the working directory contains a file/directory with a name that ends with an excluded suffix
   264  			if isExclude(path, techData) {
   265  				excludedTechAtWorkingDir[directory] = append(excludedTechAtWorkingDir[directory], tech)
   266  			}
   267  		}
   268  	}
   269  	workingDirectoryToIndicators = make(map[string][]string)
   270  	for wd, indicators := range workingDirectoryToIndicatorsSet {
   271  		workingDirectoryToIndicators[wd] = indicators.ToSlice()
   272  	}
   273  	return
   274  }
   275  
   276  func isDescriptor(path string, techData TechData) bool {
   277  	for _, descriptor := range techData.packageDescriptors {
   278  		if strings.HasSuffix(path, descriptor) {
   279  			return true
   280  		}
   281  	}
   282  	return false
   283  }
   284  
   285  func isRequestedDescriptor(path string, requestedDescriptors []string) bool {
   286  	for _, requestedDescriptor := range requestedDescriptors {
   287  		if strings.HasSuffix(path, requestedDescriptor) {
   288  			return true
   289  		}
   290  	}
   291  	return false
   292  }
   293  
   294  func isIndicator(path string, techData TechData) bool {
   295  	for _, indicator := range techData.indicators {
   296  		if strings.HasSuffix(path, indicator) {
   297  			return true
   298  		}
   299  	}
   300  	return false
   301  }
   302  
   303  func isExclude(path string, techData TechData) bool {
   304  	for _, exclude := range techData.exclude {
   305  		if strings.HasSuffix(path, exclude) {
   306  			return true
   307  		}
   308  	}
   309  	return false
   310  }
   311  
   312  // Map working directories to technologies according to the given workingDirectoryToIndicators map files.
   313  // workingDirectoryToIndicators: A map of working directories to the files inside the directory that are relevant to the technologies.
   314  // excludedTechAtWorkingDir: A map of working directories to the technologies that are excluded from the working directory.
   315  // requestedTechs: The technologies to check, if empty all technologies will be checked.
   316  // requestedDescriptors: Special requested descriptors (for example in Pip requirement.txt can have different path) for each technology to detect.
   317  func mapWorkingDirectoriesToTechnologies(workingDirectoryToIndicators map[string][]string, excludedTechAtWorkingDir map[string][]Technology, requestedTechs []Technology, requestedDescriptors map[Technology][]string) (technologiesDetected map[Technology]map[string][]string) {
   318  	// Get the relevant technologies to check
   319  	technologies := requestedTechs
   320  	if len(technologies) == 0 {
   321  		technologies = GetAllTechnologiesList()
   322  	}
   323  	technologiesDetected = make(map[Technology]map[string][]string)
   324  	// Map working directories to technologies
   325  	for _, tech := range technologies {
   326  		techWorkingDirs := getTechInformationFromWorkingDir(tech, workingDirectoryToIndicators, excludedTechAtWorkingDir, requestedDescriptors)
   327  		if len(techWorkingDirs) > 0 {
   328  			// Found indicators of the technology, add to detected.
   329  			technologiesDetected[tech] = techWorkingDirs
   330  		}
   331  	}
   332  	for _, tech := range requestedTechs {
   333  		if _, exist := technologiesDetected[tech]; !exist {
   334  			// Requested (forced with flag) technology and not found any indicators/descriptors in detection, add as detected.
   335  			log.Warn(fmt.Sprintf("Requested technology %s but not found any indicators/descriptors in detection.", tech))
   336  			technologiesDetected[tech] = map[string][]string{}
   337  		}
   338  	}
   339  	return
   340  }
   341  
   342  func getTechInformationFromWorkingDir(tech Technology, workingDirectoryToIndicators map[string][]string, excludedTechAtWorkingDir map[string][]Technology, requestedDescriptors map[Technology][]string) (techWorkingDirs map[string][]string) {
   343  	techWorkingDirs = make(map[string][]string)
   344  	for wd, indicators := range workingDirectoryToIndicators {
   345  		descriptorsAtWd := []string{}
   346  		foundIndicator := false
   347  		if isTechExcludedInWorkingDir(tech, wd, excludedTechAtWorkingDir) {
   348  			// Exclude this technology from this working directory
   349  			continue
   350  		}
   351  		// Check if the working directory contains indicators/descriptors for the technology
   352  		for _, path := range indicators {
   353  			if isDescriptor(path, technologiesData[tech]) || isRequestedDescriptor(path, requestedDescriptors[tech]) {
   354  				descriptorsAtWd = append(descriptorsAtWd, path)
   355  			}
   356  			if isIndicator(path, technologiesData[tech]) || isRequestedDescriptor(path, requestedDescriptors[tech]) {
   357  				foundIndicator = true
   358  			}
   359  		}
   360  		if foundIndicator {
   361  			// Found indicators of the technology in the current working directory, add to detected.
   362  			techWorkingDirs[wd] = descriptorsAtWd
   363  		}
   364  	}
   365  	// Don't allow working directory if sub directory already exists as key for the same technology
   366  	techWorkingDirs = cleanSubDirectories(techWorkingDirs)
   367  	return
   368  }
   369  
   370  func isTechExcludedInWorkingDir(tech Technology, wd string, excludedTechAtWorkingDir map[string][]Technology) bool {
   371  	if excludedTechs, exist := excludedTechAtWorkingDir[wd]; exist {
   372  		for _, excludedTech := range excludedTechs {
   373  			if excludedTech == tech {
   374  				return true
   375  			}
   376  		}
   377  	}
   378  	return false
   379  }
   380  
   381  // Remove sub directories keys from the given workingDirectoryToFiles map.
   382  // Keys: [dir/dir, dir/directory] -> [dir/dir, dir/directory]
   383  // Keys: [dir, directory] -> [dir, directory]
   384  // Keys: [dir/dir2, dir/dir2/dir3, dir/dir2/dir3/dir4] -> [dir/dir2]
   385  // Values of removed sub directories will be added to the root directory.
   386  func cleanSubDirectories(workingDirectoryToFiles map[string][]string) (result map[string][]string) {
   387  	result = make(map[string][]string)
   388  	for wd, files := range workingDirectoryToFiles {
   389  		root := getExistingRootDir(wd, workingDirectoryToFiles)
   390  		result[root] = append(result[root], files...)
   391  	}
   392  	return
   393  }
   394  
   395  // Get the root directory of the given path according to the given workingDirectoryToIndicators map.
   396  func getExistingRootDir(path string, workingDirectoryToIndicators map[string][]string) (root string) {
   397  	root = path
   398  	for wd := range workingDirectoryToIndicators {
   399  		parentWd := filepath.Dir(wd)
   400  		parentRoot := filepath.Dir(root)
   401  		if parentRoot != parentWd && strings.HasPrefix(root, wd) {
   402  			root = wd
   403  		}
   404  	}
   405  	return
   406  }
   407  
   408  // DetectTechnologies tries to detect all technologies types according to the files in the given path.
   409  // 'isCiSetup' will limit the search of possible techs to Maven, Gradle, and npm.
   410  // 'recursive' will determine if the search will be limited to files in the root path or not.
   411  func DetectTechnologies(path string, isCiSetup, recursive bool) (map[Technology]bool, error) {
   412  	var filesList []string
   413  	var err error
   414  	if recursive {
   415  		filesList, err = fileutils.ListFilesRecursiveWalkIntoDirSymlink(path, false)
   416  	} else {
   417  		filesList, err = fileutils.ListFiles(path, true)
   418  	}
   419  	if err != nil {
   420  		return nil, err
   421  	}
   422  	log.Info(fmt.Sprintf("Scanning %d file(s):%s", len(filesList), filesList))
   423  	detectedTechnologies := detectTechnologiesByFilePaths(filesList, isCiSetup)
   424  	return detectedTechnologies, nil
   425  }
   426  
   427  func detectTechnologiesByFilePaths(paths []string, isCiSetup bool) (detected map[Technology]bool) {
   428  	detected = make(map[Technology]bool)
   429  	exclude := make(map[Technology]bool)
   430  	for _, path := range paths {
   431  		for techName, techData := range technologiesData {
   432  			// If the detection is in a 'jf ci-setup' command, then the checked technology must be supported.
   433  			if !isCiSetup || (isCiSetup && techData.ciSetupSupport) {
   434  				// If the project contains a file/directory with a name that ends with an excluded suffix, then this technology is excluded.
   435  				for _, excludeFile := range techData.exclude {
   436  					if strings.HasSuffix(path, excludeFile) {
   437  						exclude[techName] = true
   438  					}
   439  				}
   440  				// If this technology was already excluded, there's no need to look for indicator files/directories.
   441  				if _, exist := exclude[techName]; !exist {
   442  					// If the project contains a file/directory with a name that ends with the indicator suffix, then the project probably uses this technology.
   443  					for _, indicator := range techData.indicators {
   444  						if strings.HasSuffix(path, indicator) {
   445  							detected[techName] = true
   446  						}
   447  					}
   448  				}
   449  			}
   450  		}
   451  	}
   452  	// Remove excluded technologies.
   453  	for excludeTech := range exclude {
   454  		delete(detected, excludeTech)
   455  	}
   456  	return detected
   457  }
   458  
   459  // DetectedTechnologiesToSlice returns a string slice that includes all the names of the detected technologies.
   460  func DetectedTechnologiesToSlice(detected map[Technology]bool) []string {
   461  	keys := make([]string, 0, len(detected))
   462  	for tech := range detected {
   463  		keys = append(keys, string(tech))
   464  	}
   465  	return keys
   466  }
   467  
   468  func ToTechnologies(args []string) (technologies []Technology) {
   469  	for _, argument := range args {
   470  		technologies = append(technologies, Technology(argument))
   471  	}
   472  	return
   473  }
   474  
   475  func GetAllTechnologiesList() (technologies []Technology) {
   476  	for tech := range technologiesData {
   477  		technologies = append(technologies, tech)
   478  	}
   479  	return
   480  }
   481  
   482  func ContainsApplicabilityScannableTech(technologies []Technology) bool {
   483  	for _, technology := range technologies {
   484  		if technology.ApplicabilityScannable() {
   485  			return true
   486  		}
   487  	}
   488  	return false
   489  }