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 }