diff --git a/utils/coreutils/techutils.go b/utils/coreutils/techutils.go index b1b3416f4..ab0a17570 100644 --- a/utils/coreutils/techutils.go +++ b/utils/coreutils/techutils.go @@ -1,6 +1,7 @@ package coreutils import ( + "encoding/json" "fmt" "os" "path/filepath" @@ -114,7 +115,7 @@ var technologiesData = map[Technology]TechData{ Poetry: { packageType: Pypi, indicators: []string{"pyproject.toml", "poetry.lock"}, - packageDescriptors: []string{"pyproject.toml"}, + packageDescriptors: []string{"pyproject.toml"}, packageInstallationCommand: "add", packageVersionOperator: "==", applicabilityScannable: true, @@ -203,30 +204,56 @@ func DetectedTechnologiesListInPath(path string, recursive bool) (technologies [ // If recursive is true, the search will not be limited to files in the root path. // If requestedTechs is empty, all technologies will be checked. // If excludePathPattern is not empty, files/directories that match the wildcard pattern will be excluded from the search. -func DetectTechnologiesDescriptors(path string, recursive bool, requestedTechs []string, excludePathPattern string) (technologiesDetected map[Technology]map[string][]string) { +func DetectTechnologiesDescriptors(path string, recursive bool, requestedTechs []string, requestedDescriptors map[Technology][]string, excludePathPattern string) (technologiesDetected map[Technology]map[string][]string) { filesList, err := fspatterns.ListFiles(path, recursive, false, true, excludePathPattern) if err != nil { return } - technologiesDetected = mapIndicatorsToTechnologies(mapWorkingDirectoriesToIndicators(filesList), ToTechnologies(requestedTechs)) + workingDirectoryToIndicators, excludedTechAtWorkingDir := mapFilesToRelevantWorkingDirectories(filesList, requestedDescriptors) + technologiesDetected = mapWorkingDirectoriesToTechnologies(workingDirectoryToIndicators, excludedTechAtWorkingDir, ToTechnologies(requestedTechs)) log.Debug(fmt.Sprintf("Detected %d technologies at %s: %s.", len(technologiesDetected), path, maps.Keys(technologiesDetected))) - return + return } -func mapWorkingDirectoriesToIndicators(files []string) (workingDirectoryToIndicators map[string][]string) { +func mapFilesToRelevantWorkingDirectories(files []string, requestedDescriptors map[Technology][]string) (workingDirectoryToIndicators map[string][]string, excludedTechAtWorkingDir map[string][]Technology) { workingDirectoryToIndicators = make(map[string][]string) + excludedTechAtWorkingDir = make(map[string][]Technology) for _, path := range files { directory := filepath.Dir(path) - for _, techData := range technologiesData { - if isDescriptor(path, techData) { + for tech, techData := range technologiesData { + // Check if the working directory contains indicators/descriptors for the technology + if isDescriptor(path, techData) || isRequestedDescriptor(path, requestedDescriptors[tech]) { workingDirectoryToIndicators[directory] = append(workingDirectoryToIndicators[directory], path) } else if isIndicator(path, techData) { workingDirectoryToIndicators[directory] = append(workingDirectoryToIndicators[directory], path) } + // Check if the working directory contains a file/directory with a name that ends with an excluded suffix + if isExclude(path, techData) { + excludedTechAtWorkingDir[directory] = append(excludedTechAtWorkingDir[directory], tech) + } + } + } + strJson, _ := json.MarshalIndent(workingDirectoryToIndicators, "", " ") + log.Debug(fmt.Sprintf("mapped %d working directories with indicators/descriptors:\n%s", len(workingDirectoryToIndicators), strJson)) + return +} + +func isDescriptor(path string, techData TechData) bool { + for _, descriptor := range techData.packageDescriptors { + if strings.HasSuffix(path, descriptor) { + return true + } + } + return false +} + +func isRequestedDescriptor(path string, requestedDescriptors []string) bool { + for _, requestedDescriptor := range requestedDescriptors { + if strings.HasSuffix(path, requestedDescriptor) { + return true } } - log.Debug(fmt.Sprintf("mapped indicators:\n%s", workingDirectoryToIndicators)) - return + return false } func isIndicator(path string, techData TechData) bool { @@ -238,41 +265,42 @@ func isIndicator(path string, techData TechData) bool { return false } -func isDescriptor(path string, techData TechData) bool { - for _, descriptor := range techData.packageDescriptors { - if strings.HasSuffix(path, descriptor) { +func isExclude(path string, techData TechData) bool { + for _, exclude := range techData.exclude { + if strings.HasSuffix(path, exclude) { return true } } return false } -func mapIndicatorsToTechnologies(workingDirectoryToIndicators map[string][]string, requestedTechs []Technology) (technologiesDetected map[Technology]map[string][]string) { - +func mapWorkingDirectoriesToTechnologies(workingDirectoryToIndicators map[string][]string, excludedTechAtWorkingDir map[string][]Technology, requestedTechs []Technology) (technologiesDetected map[Technology]map[string][]string) { + // Get the relevant technologies to check technologies := requestedTechs if len(technologies) == 0 { technologies = GetAllTechnologiesList() } technologiesDetected = make(map[Technology]map[string][]string) - + // Map working directories to technologies for _, tech := range technologies { techWorkingDirs := make(map[string][]string) foundIndicator := false for wd, indicators := range workingDirectoryToIndicators { - - - // What to do about no descriptors found and only indicators? - // Implement exclude - // Collect only descriptors, indicators adds techWorkingDirs - - // Map tech for indicators/descriptors to tech + if excludedTechs, exist := excludedTechAtWorkingDir[wd]; exist { + for _, excludedTech := range excludedTechs { + if excludedTech == tech { + // Exclude this technology from this working directory + continue + } + } + } + // Check if the working directory contains indicators/descriptors for the technology for _, path := range indicators { if isDescriptor(path, technologiesData[tech]) { techWorkingDirs[wd] = append(techWorkingDirs[wd], path) + } else if isIndicator(path, technologiesData[tech]) { + foundIndicator = true } - // else if isIndicator(path, technologiesData[tech]) { - // foundIndicator = true - // } } } // Don't allow working directory if sub directory already exists as key for the same technology @@ -286,29 +314,45 @@ func mapIndicatorsToTechnologies(workingDirectoryToIndicators map[string][]strin for _, tech := range requestedTechs { if _, exist := technologiesDetected[tech]; !exist { // Requested (forced with flag) technology and not found any indicators/descriptors in detection, add as detected. + log.Warn(fmt.Sprintf("Requested technology %s but not found any indicators/descriptors in detection.", tech)) technologiesDetected[tech] = map[string][]string{} } } return } -func cleanSubDirectories(workingDirectoryToIndicators map[string][]string) (result map[string][]string) { +func cleanSubDirectories(workingDirectoryToFiles map[string][]string) (result map[string][]string) { result = make(map[string][]string) - for wd, indicators := range workingDirectoryToIndicators { - if !hasSubDirKey(wd, workingDirectoryToIndicators) { - result[wd] = indicators - } + for wd, files := range workingDirectoryToFiles { + root := getExistingRootDir(wd, workingDirectoryToFiles) + result[root] = append(result[root], files...) + // if root == wd { + // // Current working directory is the root + // result[wd] = files + // } else { + // // add descriptors from sub projects to the root + // result[root] = append(result[root], files...) + // } } return } -func hasSubDirKey(dir string, workingDirectoryToIndicators map[string][]string) bool { +func getExistingRootDir(path string, workingDirectoryToIndicators map[string][]string) (rootDir string) { + rootDir = path for wd := range workingDirectoryToIndicators { - if dir != wd && strings.HasPrefix(dir, wd) { - return true + if strings.HasPrefix(rootDir, wd) { + rootDir = wd } } - return false + return + + // // TODO: make sure to get the top most root! + // for wd := range workingDirectoryToIndicators { + // if path != wd && strings.HasPrefix(path, wd) { + // return wd + // } + // } + // return "" } // func detectTechnologiesDescriptorsByFilePaths(paths []string) (technologiesToDescriptors map[Technology][]string) { diff --git a/xray/commands/audit/scarunner.go b/xray/commands/audit/scarunner.go index 5f6f337da..ccad27e24 100644 --- a/xray/commands/audit/scarunner.go +++ b/xray/commands/audit/scarunner.go @@ -50,24 +50,23 @@ func scaScan(params *AuditParams, results *xrayutils.Results) (err error) { printScansInformation(scans) for _, scan := range scans { // Run the scan - log.Info("Running SCA scan for", scan.Technology ,"vulnerable dependencies in", scan.WorkingDirectory, "directory...") - if wdScanErr := executeScaScan(serverDetails, params, &scan); wdScanErr != nil { + log.Info("Running SCA scan for", scan.Technology, "vulnerable dependencies in", scan.WorkingDirectory, "directory...") + if wdScanErr := executeScaScan(serverDetails, params, scan); wdScanErr != nil { err = errors.Join(err, fmt.Errorf("audit command in '%s' failed:\n%s", scan.WorkingDirectory, wdScanErr.Error())) continue } // Add the scan to the results - results.ScaResults = append(results.ScaResults, scan) + results.ScaResults = append(results.ScaResults, *scan) } return } -func getScaScansToPreform(currentWorkingDir string, params *AuditParams) (scansToPreform []xrayutils.ScaScanResult) { +func getScaScansToPreform(currentWorkingDir string, params *AuditParams) (scansToPreform []*xrayutils.ScaScanResult) { isRecursive := true excludePattern := "" - for _, requestedDirectory := range getRequestedDirectoriesToScan(currentWorkingDir, params) { // Detect descriptors from files - techToWorkingDirs := coreutils.DetectTechnologiesDescriptors(requestedDirectory, isRecursive, params.Technologies(), excludePattern) + techToWorkingDirs := coreutils.DetectTechnologiesDescriptors(requestedDirectory, isRecursive, params.Technologies(), getRequestedDescriptors(params), excludePattern) // Create scans to preform for tech, workingDirs := range techToWorkingDirs { if tech == coreutils.Dotnet { @@ -77,20 +76,30 @@ func getScaScansToPreform(currentWorkingDir string, params *AuditParams) (scansT } if len(workingDirs) == 0 { // Requested technology (from params) descriptors was not found, scan only requested directory for this technology. - scansToPreform = append(scansToPreform, xrayutils.ScaScanResult{WorkingDirectory: requestedDirectory, Technology: tech}) + scansToPreform = append(scansToPreform, &xrayutils.ScaScanResult{WorkingDirectory: requestedDirectory, Technology: tech}) } for workingDir, descriptors := range workingDirs { // Add scan for each detected working directory. - scansToPreform = append(scansToPreform, xrayutils.ScaScanResult{WorkingDirectory: workingDir, Technology: tech, Descriptors: descriptors}) + scansToPreform = append(scansToPreform, &xrayutils.ScaScanResult{WorkingDirectory: workingDir, Technology: tech, Descriptors: descriptors}) } } } return } -func printScansInformation(scans []xrayutils.ScaScanResult) { - scansJson, _ := json.MarshalIndent(scans, "", " ") - log.Info(fmt.Sprintf("Preforming %d SCA scans:\n%s", len(scans), string(scansJson))) +func getRequestedDescriptors(params *AuditParams) map[coreutils.Technology][]string { + requestedDescriptors := map[coreutils.Technology][]string{} + if params.PipRequirementsFile() != "" { + requestedDescriptors[coreutils.Pip] = []string{params.PipRequirementsFile()} + } + return requestedDescriptors +} + +func printScansInformation(scans []*xrayutils.ScaScanResult) { + scansJson, err := json.MarshalIndent(scans, "", " ") + if err == nil { + log.Info(fmt.Sprintf("Preforming %d SCA scans:\n%s", len(scans), string(scansJson))) + } } func getRequestedDirectoriesToScan(currentWorkingDir string, params *AuditParams) []string { @@ -182,14 +191,14 @@ func runScaWithTech(tech coreutils.Technology, params *AuditParams, serverDetail // return // } -func getTechnologiesToDetect(params *AuditParams) (technologies []coreutils.Technology) { - if len(params.Technologies()) != 0 { - technologies = coreutils.ToTechnologies(params.Technologies()) - } else { - technologies = coreutils.GetAllTechnologiesList() - } - return -} +// func getTechnologiesToDetect(params *AuditParams) (technologies []coreutils.Technology) { +// if len(params.Technologies()) != 0 { +// technologies = coreutils.ToTechnologies(params.Technologies()) +// } else { +// technologies = coreutils.GetAllTechnologiesList() +// } +// return +// } func addThirdPartyDependenciesToParams(params *AuditParams, tech coreutils.Technology, flatTree *xrayCmdUtils.GraphNode, fullDependencyTrees []*xrayCmdUtils.GraphNode) { var dependenciesForApplicabilityScan []string @@ -201,117 +210,117 @@ func addThirdPartyDependenciesToParams(params *AuditParams, tech coreutils.Techn params.AppendDependenciesForApplicabilityScan(dependenciesForApplicabilityScan) } -func runScaOnTech(tech coreutils.Technology, params *AuditParams, serverDetails *config.ServerDetails, flatTree *xrayCmdUtils.GraphNode, fullDependencyTrees []*xrayCmdUtils.GraphNode, results *xrayutils.Results) (techResults []services.ScanResponse, err error) { - scanGraphParams := scangraph.NewScanGraphParams(). - SetServerDetails(serverDetails). - SetXrayGraphScanParams(params.xrayGraphScanParams). - SetXrayVersion(params.xrayVersion). - SetFixableOnly(params.fixableOnly). - SetSeverityLevel(params.minSeverityFilter) - techResults, err = sca.RunXrayDependenciesTreeScanGraph(flatTree, params.Progress(), tech, scanGraphParams) - if err != nil { - return - } - techResults = sca.BuildImpactPathsForScanResponse(techResults, fullDependencyTrees) +// func runScaOnTech(tech coreutils.Technology, params *AuditParams, serverDetails *config.ServerDetails, flatTree *xrayCmdUtils.GraphNode, fullDependencyTrees []*xrayCmdUtils.GraphNode, results *xrayutils.Results) (techResults []services.ScanResponse, err error) { +// scanGraphParams := scangraph.NewScanGraphParams(). +// SetServerDetails(serverDetails). +// SetXrayGraphScanParams(params.xrayGraphScanParams). +// SetXrayVersion(params.xrayVersion). +// SetFixableOnly(params.fixableOnly). +// SetSeverityLevel(params.minSeverityFilter) +// techResults, err = sca.RunXrayDependenciesTreeScanGraph(flatTree, params.Progress(), tech, scanGraphParams) +// if err != nil { +// return +// } +// techResults = sca.BuildImpactPathsForScanResponse(techResults, fullDependencyTrees) - // results.ExtendedScanResults.XrayResults = append(results.ExtendedScanResults.XrayResults, techResults...) - // if !results.IsMultipleRootProject { - // results.IsMultipleRootProject = len(fullDependencyTrees) > 1 - // } +// // results.ExtendedScanResults.XrayResults = append(results.ExtendedScanResults.XrayResults, techResults...) +// // if !results.IsMultipleRootProject { +// // results.IsMultipleRootProject = len(fullDependencyTrees) > 1 +// // } - // results.ExtendedScanResults.ScannedTechnologies = append(results.ExtendedScanResults.ScannedTechnologies, tech) - return -} +// // results.ExtendedScanResults.ScannedTechnologies = append(results.ExtendedScanResults.ScannedTechnologies, tech) +// return +// } -func runScaScan(params *AuditParams, results *xrayutils.Results) (err error) { - rootDir, err := os.Getwd() - if errorutils.CheckError(err) != nil { - return - } - for _, wd := range params.workingDirs { - if len(params.workingDirs) > 1 { - log.Info("Running SCA scan for vulnerable dependencies scan in", wd, "directory...") - } else { - log.Info("Running SCA scan for vulnerable dependencies...") - } - wdScanErr := runScaScanOnWorkingDir(params, results, wd, rootDir) - if wdScanErr != nil { - err = errors.Join(err, fmt.Errorf("audit command in '%s' failed:\n%s\n", wd, wdScanErr.Error())) - continue - } - } - return -} +// func runScaScan(params *AuditParams, results *xrayutils.Results) (err error) { +// rootDir, err := os.Getwd() +// if errorutils.CheckError(err) != nil { +// return +// } +// for _, wd := range params.workingDirs { +// if len(params.workingDirs) > 1 { +// log.Info("Running SCA scan for vulnerable dependencies scan in", wd, "directory...") +// } else { +// log.Info("Running SCA scan for vulnerable dependencies...") +// } +// wdScanErr := runScaScanOnWorkingDir(params, results, wd, rootDir) +// if wdScanErr != nil { +// err = errors.Join(err, fmt.Errorf("audit command in '%s' failed:\n%s\n", wd, wdScanErr.Error())) +// continue +// } +// } +// return +// } // Audits the project found in the current directory using Xray. -func runScaScanOnWorkingDir(params *AuditParams, results *xrayutils.Results, workingDir, rootDir string) (err error) { - err = os.Chdir(workingDir) - if err != nil { - return - } - defer func() { - err = errors.Join(err, os.Chdir(rootDir)) - }() +// func runScaScanOnWorkingDir(params *AuditParams, results *xrayutils.Results, workingDir, rootDir string) (err error) { +// err = os.Chdir(workingDir) +// if err != nil { +// return +// } +// defer func() { +// err = errors.Join(err, os.Chdir(rootDir)) +// }() - var technologies []string - requestedTechnologies := params.Technologies() - if len(requestedTechnologies) != 0 { - technologies = requestedTechnologies - } else { - technologies = coreutils.DetectedTechnologiesList() - } - if len(technologies) == 0 { - log.Info("Couldn't determine a package manager or build tool used by this project. Skipping the SCA scan...") - return - } - serverDetails, err := params.ServerDetails() - if err != nil { - return - } +// var technologies []string +// requestedTechnologies := params.Technologies() +// if len(requestedTechnologies) != 0 { +// technologies = requestedTechnologies +// } else { +// technologies = coreutils.DetectedTechnologiesList() +// } +// if len(technologies) == 0 { +// log.Info("Couldn't determine a package manager or build tool used by this project. Skipping the SCA scan...") +// return +// } +// serverDetails, err := params.ServerDetails() +// if err != nil { +// return +// } - for _, tech := range coreutils.ToTechnologies(technologies) { - if tech == coreutils.Dotnet { - continue - } - flattenTree, fullDependencyTrees, techErr := GetTechDependencyTree(params.AuditBasicParams, tech) - if techErr != nil { - err = errors.Join(err, fmt.Errorf("failed while building '%s' dependency tree:\n%s\n", tech, techErr.Error())) - continue - } - if flattenTree == nil || len(flattenTree.Nodes) == 0 { - err = errors.Join(err, errors.New("no dependencies were found. Please try to build your project and re-run the audit command")) - continue - } +// for _, tech := range coreutils.ToTechnologies(technologies) { +// if tech == coreutils.Dotnet { +// continue +// } +// flattenTree, fullDependencyTrees, techErr := GetTechDependencyTree(params.AuditBasicParams, tech) +// if techErr != nil { +// err = errors.Join(err, fmt.Errorf("failed while building '%s' dependency tree:\n%s\n", tech, techErr.Error())) +// continue +// } +// if flattenTree == nil || len(flattenTree.Nodes) == 0 { +// err = errors.Join(err, errors.New("no dependencies were found. Please try to build your project and re-run the audit command")) +// continue +// } - scanGraphParams := scangraph.NewScanGraphParams(). - SetServerDetails(serverDetails). - SetXrayGraphScanParams(params.xrayGraphScanParams). - SetXrayVersion(params.xrayVersion). - SetFixableOnly(params.fixableOnly). - SetSeverityLevel(params.minSeverityFilter) - techResults, techErr := sca.RunXrayDependenciesTreeScanGraph(flattenTree, params.Progress(), tech, scanGraphParams) - if techErr != nil { - err = errors.Join(err, fmt.Errorf("'%s' Xray dependency tree scan request failed:\n%s\n", tech, techErr.Error())) - continue - } - techResults = sca.BuildImpactPathsForScanResponse(techResults, fullDependencyTrees) +// scanGraphParams := scangraph.NewScanGraphParams(). +// SetServerDetails(serverDetails). +// SetXrayGraphScanParams(params.xrayGraphScanParams). +// SetXrayVersion(params.xrayVersion). +// SetFixableOnly(params.fixableOnly). +// SetSeverityLevel(params.minSeverityFilter) +// techResults, techErr := sca.RunXrayDependenciesTreeScanGraph(flattenTree, params.Progress(), tech, scanGraphParams) +// if techErr != nil { +// err = errors.Join(err, fmt.Errorf("'%s' Xray dependency tree scan request failed:\n%s\n", tech, techErr.Error())) +// continue +// } +// techResults = sca.BuildImpactPathsForScanResponse(techResults, fullDependencyTrees) - var dependenciesForApplicabilityScan []string - if shouldUseAllDependencies(params.thirdPartyApplicabilityScan, tech) { - dependenciesForApplicabilityScan = getDirectDependenciesFromTree([]*xrayCmdUtils.GraphNode{flattenTree}) - } else { - dependenciesForApplicabilityScan = getDirectDependenciesFromTree(fullDependencyTrees) - } - params.AppendDependenciesForApplicabilityScan(dependenciesForApplicabilityScan) +// var dependenciesForApplicabilityScan []string +// if shouldUseAllDependencies(params.thirdPartyApplicabilityScan, tech) { +// dependenciesForApplicabilityScan = getDirectDependenciesFromTree([]*xrayCmdUtils.GraphNode{flattenTree}) +// } else { +// dependenciesForApplicabilityScan = getDirectDependenciesFromTree(fullDependencyTrees) +// } +// params.AppendDependenciesForApplicabilityScan(dependenciesForApplicabilityScan) - // results.ExtendedScanResults.XrayResults = append(results.ExtendedScanResults.XrayResults, techResults...) - // if !results.IsMultipleRootProject { - // results.IsMultipleRootProject = len(fullDependencyTrees) > 1 - // } - // results.ExtendedScanResults.ScannedTechnologies = append(results.ExtendedScanResults.ScannedTechnologies, tech) - } - return -} +// // results.ExtendedScanResults.XrayResults = append(results.ExtendedScanResults.XrayResults, techResults...) +// // if !results.IsMultipleRootProject { +// // results.IsMultipleRootProject = len(fullDependencyTrees) > 1 +// // } +// // results.ExtendedScanResults.ScannedTechnologies = append(results.ExtendedScanResults.ScannedTechnologies, tech) +// } +// return +// } // When building pip dependency tree using pipdeptree, some of the direct dependencies are recognized as transitive and missed by the CA scanner. // Our solution for this case is to send all dependencies to the CA scanner. diff --git a/xray/utils/results.go b/xray/utils/results.go index 1b03236e4..c86a3c0d3 100644 --- a/xray/utils/results.go +++ b/xray/utils/results.go @@ -8,13 +8,13 @@ import ( ) type Results struct { - ScaResults []ScaScanResult - XrayVersion string - ScaError error - + ScaResults []ScaScanResult + XrayVersion string + ScaError error + // IsMultipleRootProject bool // TODO: remove this - ExtendedScanResults *ExtendedScanResults - JasError error + ExtendedScanResults *ExtendedScanResults + JasError error } func NewAuditResults() *Results { @@ -67,12 +67,12 @@ func (r *Results) IsIssuesFound() bool { } type ScaScanResult struct { - Technology coreutils.Technology `json:"Technology"` - WorkingDirectory string `json:"WorkingDirectory"` - Descriptors []string `json:"Descriptors"` - XrayResults []services.ScanResponse `json:"XrayResults,omitempty"` + Technology coreutils.Technology `json:"Technology"` + WorkingDirectory string `json:"WorkingDirectory"` + Descriptors []string `json:"Descriptors,omitempty"` + XrayResults []services.ScanResponse `json:"XrayResults,omitempty"` - IsMultipleRootProject *bool `json:"IsMultipleRootProject,omitempty"` + IsMultipleRootProject *bool `json:"IsMultipleRootProject,omitempty"` } // func (s ScaScanResult) IsMultipleRootProject() bool { @@ -101,12 +101,12 @@ type ExtendedScanResults struct { } func (e *ExtendedScanResults) IsIssuesFound() bool { - return GetResultsLocationCount(e.ApplicabilityScanResults...) > 0 || - GetResultsLocationCount(e.SecretsScanResults...) > 0 || - GetResultsLocationCount(e.IacScanResults...) > 0 || - GetResultsLocationCount(e.SastScanResults...) > 0 + return GetResultsLocationCount(e.ApplicabilityScanResults...) > 0 || + GetResultsLocationCount(e.SecretsScanResults...) > 0 || + GetResultsLocationCount(e.IacScanResults...) > 0 || + GetResultsLocationCount(e.SastScanResults...) > 0 } // func (e *ExtendedScanResults) getXrayScanResults() []services.ScanResponse { // return e.XrayResults -// } \ No newline at end of file +// }