diff --git a/.github/workflows/frogbot-scan-and-fix.yml b/.github/workflows/frogbot-scan-and-fix.yml deleted file mode 100644 index aa34c12cc..000000000 --- a/.github/workflows/frogbot-scan-and-fix.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: "Frogbot Scan and Fix" -on: - schedule: - # The repository will be scanned once a day at 00:00 GMT. - - cron: "0 0 * * *" -permissions: - contents: write - pull-requests: write - security-events: write -jobs: - create-fix-pull-requests: - runs-on: ubuntu-latest - strategy: - matrix: - # The repository scanning will be triggered periodically on the following branches. - branch: [ "dev" ] - steps: - - uses: actions/checkout@v3 - with: - ref: ${{ matrix.branch }} - - # Install prerequisites - - name: Setup Go - uses: actions/setup-go@v3 - with: - go-version: 1.20.x - - - uses: jfrog/frogbot@v2 - env: - # [Mandatory] - # JFrog platform URL - JF_URL: ${{ secrets.FROGBOT_URL }} - - # [Mandatory if JF_USER and JF_PASSWORD are not provided] - # JFrog access token with 'read' permissions on Xray service - JF_ACCESS_TOKEN: ${{ secrets.FROGBOT_ACCESS_TOKEN }} - - # [Mandatory] - # The GitHub token automatically generated for the job - JF_GIT_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/frogbot-scan-pr.yml b/.github/workflows/frogbot-scan-pr.yml deleted file mode 100644 index d57c83e98..000000000 --- a/.github/workflows/frogbot-scan-pr.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: "Frogbot Scan Pull Request" -on: - pull_request_target: - types: [ opened, synchronize ] -permissions: - pull-requests: write - contents: read -jobs: - scan-pull-request: - runs-on: ubuntu-latest - # A pull request needs to be approved, before Frogbot scans it. Any GitHub user who is associated with the - # "frogbot" GitHub environment can approve the pull request to be scanned. - environment: frogbot - steps: - - uses: actions/checkout@v3 - with: - ref: ${{ github.event.pull_request.head.sha }} - - # Install prerequisites - - name: Setup Go - uses: actions/setup-go@v3 - with: - go-version: 1.20.x - - - uses: jfrog/frogbot@v2 - env: - # [Mandatory] - # JFrog platform URL (This functionality requires version 3.29.0 or above of Xray) - JF_URL: ${{ secrets.FROGBOT_URL }} - - # [Mandatory if JF_USER and JF_PASSWORD are not provided] - # JFrog access token with 'read' permissions on Xray service - JF_ACCESS_TOKEN: ${{ secrets.FROGBOT_ACCESS_TOKEN }} - - # [Mandatory] - # The GitHub token automatically generated for the job - JF_GIT_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/frogbot-scan-pull-request.yml b/.github/workflows/frogbot-scan-pull-request.yml new file mode 100644 index 000000000..998c8c91f --- /dev/null +++ b/.github/workflows/frogbot-scan-pull-request.yml @@ -0,0 +1,119 @@ +name: "Frogbot Scan Pull Request" +on: + pull_request_target: + types: [ opened, synchronize ] +permissions: + pull-requests: write + contents: read +jobs: + scan-pull-request: + runs-on: ubuntu-latest + # A pull request needs to be approved before Frogbot scans it. Any GitHub user who is associated with the + # "frogbot" GitHub environment can approve the pull request to be scanned. + environment: frogbot + steps: + - uses: jfrog/frogbot@v2 + env: + JFROG_CLI_LOG_LEVEL: "DEBUG" + # [Mandatory] + # JFrog platform URL (This functionality requires version 3.29.0 or above of Xray) + JF_URL: ${{ secrets.FROGBOT_URL }} + + # [Mandatory if JF_USER and JF_PASSWORD are not provided] + # JFrog access token with 'read' permissions on Xray service + JF_ACCESS_TOKEN: ${{ secrets.FROGBOT_ACCESS_TOKEN }} + + # [Mandatory] + # The GitHub token is automatically generated for the job + JF_GIT_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # [Optional, default: https://api.github.com] + # API endpoint to GitHub + # JF_GIT_API_ENDPOINT: https://github.example.com + + # [Optional] + # By default, the Frogbot workflows download the Frogbot executable as well as other tools + # needed from https://releases.jfrog.io + # If the machine that runs Frogbot has no access to the internet, follow these steps to allow the + # executable to be downloaded from an Artifactory instance, which the machine has access to: + # + # 1. Login to the Artifactory UI, with a user who has admin credentials. + # 2. Create a Remote Repository with the following properties set. + # Under the 'Basic' tab: + # Package Type: Generic + # URL: https://releases.jfrog.io + # Under the 'Advanced' tab: + # Uncheck the 'Store Artifacts Locally' option + # 3. Set the value of the 'JF_RELEASES_REPO' variable with the Repository Key you created. + # JF_RELEASES_REPO: "" + + # [Optional] + # Configure the SMTP server to enable Frogbot to send emails with detected secrets in pull request scans. + # SMTP server URL including should the relevant port: (Example: smtp.server.com:8080) + JF_SMTP_SERVER: ${{ secrets.JF_SMTP_SERVER }} + + # [Mandatory if JF_SMTP_SERVER is set] + # The username required for authenticating with the SMTP server. + JF_SMTP_USER: ${{ secrets.JF_SMTP_USER }} + + # [Mandatory if JF_SMTP_SERVER is set] + # The password associated with the username required for authentication with the SMTP server. + JF_SMTP_PASSWORD: ${{ secrets.JF_SMTP_PASSWORD }} + + # [Optional] + # List of comma separated email addresses to receive email notifications about secrets + # detected during pull request scanning. The notification is also sent to the email set + # in the committer git profile regardless of whether this variable is set or not. + JF_EMAIL_RECEIVERS: "eco-system@jfrog.com" + + ########################################################################## + ## If your project uses a 'frogbot-config.yml' file, you can define ## + ## the following variables inside the file, instead of here. ## + ########################################################################## + + # [Mandatory if the two conditions below are met] + # 1. The project uses yarn 2, NuGet or .NET Core to download its dependencies + # 2. The `installCommand` variable isn't set in your frogbot-config.yml file. + # + # The command that installs the project dependencies (e.g "nuget restore") + # JF_INSTALL_DEPS_CMD: "" + + # [Optional, default: "."] + # Relative path to the root of the project in the Git repository + # JF_WORKING_DIR: path/to/project/dir + + # [Optional] + # Xray Watches. Learn more about them here: https://www.jfrog.com/confluence/display/JFROG/Configuring+Xray+Watches + # JF_WATCHES: ,... + + # [Optional] + # JFrog project. Learn more about it here: https://www.jfrog.com/confluence/display/JFROG/Projects + # JF_PROJECT: + + # [Optional, default: "FALSE"] + # Displays all existing vulnerabilities, including the ones that were added by the pull request. + # JF_INCLUDE_ALL_VULNERABILITIES: "TRUE" + + # [Optional, default: "TRUE"] + # Fails the Frogbot task if any security issue is found. + # JF_FAIL: "FALSE" + + # [Optional] + # Frogbot will download the project dependencies if they're not cached locally. To download the + # dependencies from a virtual repository in Artifactory, set the name of the repository. There's no + # need to set this value, if it is set in the frogbot-config.yml file. + # JF_DEPS_REPO: "" + + # [Optional, Default: "FALSE"] + # If TRUE, Frogbot creates a single pull request with all the fixes. + # If false, Frogbot creates a separate pull request for each fix. + # JF_GIT_AGGREGATE_FIXES: "FALSE" + + # [Optional, Default: "FALSE"] + # Handle vulnerabilities with fix versions only + # JF_FIXABLE_ONLY: "TRUE" + + # [Optional] + # Set the minimum severity for vulnerabilities that should be fixed and commented on in pull requests + # The following values are accepted: Low, Medium, High or Critical + # JF_MIN_SEVERITY: "" \ No newline at end of file diff --git a/.github/workflows/frogbot-scan-repository.yml b/.github/workflows/frogbot-scan-repository.yml new file mode 100644 index 000000000..01b568f67 --- /dev/null +++ b/.github/workflows/frogbot-scan-repository.yml @@ -0,0 +1,125 @@ +name: "Frogbot Scan Repository" +on: + workflow_dispatch: + schedule: + # The repository will be scanned once a day at 00:00 GMT. + - cron: "0 0 * * *" +permissions: + contents: write + pull-requests: write + security-events: write +jobs: + scan-repository: + runs-on: ubuntu-latest + strategy: + matrix: + # The repository scanning will be triggered periodically on the following branches. + branch: [ "dev" ] + steps: + - uses: jfrog/frogbot@v2 + env: + JFROG_CLI_LOG_LEVEL: "DEBUG" + # [Mandatory] + # JFrog platform URL (This functionality requires version 3.29.0 or above of Xray) + JF_URL: ${{ secrets.FROGBOT_URL }} + + # [Mandatory if JF_USER and JF_PASSWORD are not provided] + # JFrog access token with 'read' permissions on Xray service + JF_ACCESS_TOKEN: ${{ secrets.FROGBOT_ACCESS_TOKEN }} + + # [Mandatory if JF_ACCESS_TOKEN is not provided] + # JFrog username with 'read' permissions for Xray. Must be provided with JF_PASSWORD + # JF_USER: ${{ secrets.JF_USER }} + + # [Mandatory if JF_ACCESS_TOKEN is not provided] + # JFrog password. Must be provided with JF_USER + # JF_PASSWORD: ${{ secrets.JF_PASSWORD }} + + # [Mandatory] + # The GitHub token is automatically generated for the job + JF_GIT_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # [Mandatory] + # The name of the branch on which Frogbot will perform the scan + JF_GIT_BASE_BRANCH: ${{ matrix.branch }} + + # [Optional, default: https://api.github.com] + # API endpoint to GitHub + # JF_GIT_API_ENDPOINT: https://github.example.com + + # [Optional] + # By default, the Frogbot workflows download the Frogbot executable as well as other tools + # needed from https://releases.jfrog.io + # If the machine that runs Frogbot has no access to the internet, follow these steps to allow the + # executable to be downloaded from an Artifactory instance, which the machine has access to: + # + # 1. Login to the Artifactory UI, with a user who has admin credentials. + # 2. Create a Remote Repository with the following properties set. + # Under the 'Basic' tab: + # Package Type: Generic + # URL: https://releases.jfrog.io + # Under the 'Advanced' tab: + # Uncheck the 'Store Artifacts Locally' option + # 3. Set the value of the 'JF_RELEASES_REPO' variable with the Repository Key you created. + # JF_RELEASES_REPO: "" + + ########################################################################## + ## If your project uses a 'frogbot-config.yml' file, you can define ## + ## the following variables inside the file, instead of here. ## + ########################################################################## + + # [Optional, default: "."] + # Relative path to the root of the project in the Git repository + # JF_WORKING_DIR: path/to/project/dir + + # [Optional] + # Xray Watches. Learn more about them here: https://www.jfrog.com/confluence/display/JFROG/Configuring+Xray+Watches + # JF_WATCHES: ,... + + # [Optional] + # JFrog project. Learn more about it here: https://www.jfrog.com/confluence/display/JFROG/Projects + # JF_PROJECT: + + # [Optional, default: "TRUE"] + # Fails the Frogbot task if any security issue is found. + # JF_FAIL: "FALSE" + + # [Optional] + # Frogbot will download the project dependencies, if they're not cached locally. To download the + # dependencies from a virtual repository in Artifactory, set the name of the repository. There's no + # need to set this value, if it is set in the frogbot-config.yml file. + # JF_DEPS_REPO: "" + + # [Optional] + # Template for the branch name generated by Frogbot when creating pull requests with fixes. + # The template must include ${BRANCH_NAME_HASH}, to ensure that the generated branch name is unique. + # The template can optionally include the ${IMPACTED_PACKAGE} and ${FIX_VERSION} variables. + # JF_BRANCH_NAME_TEMPLATE: "frogbot-${IMPACTED_PACKAGE}-${BRANCH_NAME_HASH}" + + # [Optional] + # Template for the commit message generated by Frogbot when creating pull requests with fixes + # The template can optionally include the ${IMPACTED_PACKAGE} and ${FIX_VERSION} variables. + # JF_COMMIT_MESSAGE_TEMPLATE: "Upgrade ${IMPACTED_PACKAGE} to ${FIX_VERSION}" + + # [Optional] + # Template for the pull request title generated by Frogbot when creating pull requests with fixes. + # The template can optionally include the ${IMPACTED_PACKAGE} and ${FIX_VERSION} variables. + # JF_PULL_REQUEST_TITLE_TEMPLATE: "[🐸 Frogbot] Upgrade ${IMPACTED_PACKAGE} to ${FIX_VERSION}" + + # [Optional, Default: "FALSE"] + # If TRUE, Frogbot creates a single pull request with all the fixes. + # If FALSE, Frogbot creates a separate pull request for each fix. + # JF_GIT_AGGREGATE_FIXES: "FALSE" + + # [Optional, Default: "FALSE"] + # Handle vulnerabilities with fix versions only + # JF_FIXABLE_ONLY: "TRUE" + + # [Optional] + # Set the minimum severity for vulnerabilities that should be fixed and commented on in pull requests + # The following values are accepted: Low, Medium, High or Critical + # JF_MIN_SEVERITY: "" + + # [Optional, Default: eco-system+frogbot@jfrog.com] + # Set the email of the commit author + # JF_GIT_EMAIL_AUTHOR: "" \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e06472f52..a729bbc44 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ ubuntu, windows, macOS ] + os: [ ubuntu, windows, macos ] env: GOPROXY: direct GRADLE_OPTS: -Dorg.gradle.daemon=false diff --git a/artifactory/commands/golang/go.go b/artifactory/commands/golang/go.go index 22dda146f..6395c5b0c 100644 --- a/artifactory/commands/golang/go.go +++ b/artifactory/commands/golang/go.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/jfrog/build-info-go/build" biutils "github.com/jfrog/build-info-go/utils" "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" "github.com/jfrog/jfrog-cli-core/v2/utils/config" @@ -15,7 +16,9 @@ import ( "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" "github.com/jfrog/jfrog-client-go/utils/log" + "io/fs" "net/http" + "os" "path" "path/filepath" "strings" @@ -131,39 +134,37 @@ func (gc *GoCommand) extractNoFallbackFromArgs() (cleanArgs []string, err error) return } -func (gc *GoCommand) run() error { - err := goutils.LogGoVersion() +func (gc *GoCommand) run() (err error) { + err = goutils.LogGoVersion() if err != nil { - return err + return } goBuildInfo, err := utils.PrepareBuildPrerequisites(gc.buildConfiguration) if err != nil { - return err + return } defer func() { if goBuildInfo != nil && err != nil { - e := goBuildInfo.Clean() - if e != nil { - err = errors.New(err.Error() + "\n" + e.Error()) - } + err = errors.Join(err, goBuildInfo.Clean()) } }() resolverDetails, err := gc.resolverParams.ServerDetails() if err != nil { - return err + return } repoUrl, err := goutils.GetArtifactoryRemoteRepoUrl(resolverDetails, gc.resolverParams.TargetRepo()) if err != nil { - return err + return } // If noFallback=false, missing packages will be fetched directly from VCS if !gc.noFallback { repoUrl += "|direct" } err = biutils.RunGo(gc.goArg, repoUrl) - if err != nil { - return coreutils.ConvertExitCodeError(errorutils.CheckError(err)) + if errorutils.CheckError(err) != nil { + err = coreutils.ConvertExitCodeError(err) + return } if goBuildInfo != nil { @@ -172,39 +173,39 @@ func (gc *GoCommand) run() error { if isGoGetCommand := len(gc.goArg) > 0 && gc.goArg[0] == "get"; isGoGetCommand { if len(gc.goArg) < 2 { // Package name was not supplied. Invalid go get commend - return errorutils.CheckErrorf("Invalid get command. Package name is missing") + err = errorutils.CheckErrorf("Invalid get command. Package name is missing") + return } tempDirPath, err = fileutils.CreateTempDir() if err != nil { - return err + return } // Cleanup the temp working directory at the end. defer func() { - e := fileutils.RemoveTempDir(tempDirPath) - if err == nil { - err = e - } + err = errors.Join(err, fileutils.RemoveTempDir(tempDirPath)) }() - serverDetails, err := resolverDetails.CreateArtAuthConfig() + var serverDetails auth.ServiceDetails + serverDetails, err = resolverDetails.CreateArtAuthConfig() if err != nil { - return err + return } err = copyGoPackageFiles(tempDirPath, gc.goArg[1], gc.resolverParams.TargetRepo(), serverDetails) if err != nil { - return err + return } } - goModule, err := goBuildInfo.AddGoModule(tempDirPath) - if err != nil { - return errorutils.CheckError(err) + var goModule *build.GoModule + goModule, err = goBuildInfo.AddGoModule(tempDirPath) + if errorutils.CheckError(err) != nil { + return } if gc.buildConfiguration.GetModule() != "" { goModule.SetName(gc.buildConfiguration.GetModule()) } - return errorutils.CheckError(goModule.CalcDependencies()) + err = errorutils.CheckError(goModule.CalcDependencies()) } - return err + return } // copyGoPackageFiles copies the package files from the go mod cache directory to the given destPath. @@ -215,11 +216,21 @@ func copyGoPackageFiles(destPath, packageName, rtTargetRepo string, authArtDetai return err } // Copy the entire content of the relevant Go pkg directory to the requested destination path. - err = fileutils.CopyDir(packageFilesPath, destPath, true, nil) + err = biutils.CopyDir(packageFilesPath, destPath, true, nil) if err != nil { return fmt.Errorf("couldn't find suitable package files: %s", packageFilesPath) } - return nil + // Set permission recursively + return filepath.WalkDir(destPath, func(path string, info fs.DirEntry, err error) error { + if err != nil { + return err + } + err = os.Chmod(path, 0700) + if err != nil { + return err + } + return nil + }) } // getPackageFilePathFromArtifactory returns a string that represents the package files cache path. @@ -249,11 +260,8 @@ func getPackageFilePathFromArtifactory(packageName, rtTargetRepo string, authArt return } } - path, err := getFileSystemPackagePath(packageCachePath, name, version) - if err != nil { - return "", err - } - return path, nil + packageFilesPath, err = getFileSystemPackagePath(packageCachePath, name, version) + return } @@ -315,7 +323,7 @@ func getFileSystemPackagePath(packageCachePath, name, version string) (string, e path, _ = filepath.Split(path) path = strings.TrimSuffix(path, separator) } - return "", errors.New("Could not find package:" + name + " in:" + packageCachePath) + return "", errors.New("Could not find package: " + name + " in: " + packageCachePath) } // buildPackageVersionRequest returns a string representing the version request to Artifactory. diff --git a/artifactory/commands/npm/npmcommand.go b/artifactory/commands/npm/npmcommand.go index ca69e787b..c6b97cff7 100644 --- a/artifactory/commands/npm/npmcommand.go +++ b/artifactory/commands/npm/npmcommand.go @@ -2,6 +2,7 @@ package npm import ( "bufio" + "errors" "fmt" "github.com/jfrog/build-info-go/build" biutils "github.com/jfrog/build-info-go/build/utils" @@ -159,7 +160,7 @@ func (ca *NpmCommand) PreparePrerequisites(repo string) error { } func (ca *NpmCommand) setRestoreNpmrcFunc() error { - restoreNpmrcFunc, err := commandUtils.BackupFile(filepath.Join(ca.workingDirectory, npmrcFileName), filepath.Join(ca.workingDirectory, npmrcBackupFileName)) + restoreNpmrcFunc, err := commandUtils.BackupFile(filepath.Join(ca.workingDirectory, npmrcFileName), npmrcBackupFileName) if err != nil { return err } @@ -169,7 +170,7 @@ func (ca *NpmCommand) setRestoreNpmrcFunc() error { } return restoreNpmrcFunc() } - return err + return nil } func (ca *NpmCommand) setArtifactoryAuth() error { @@ -280,10 +281,7 @@ func (ca *NpmCommand) Run() (err error) { return } defer func() { - e := ca.restoreNpmrcFunc() - if err == nil { - err = e - } + err = errors.Join(err, ca.restoreNpmrcFunc()) }() if err = ca.CreateTempNpmrc(); err != nil { return @@ -293,9 +291,7 @@ func (ca *NpmCommand) Run() (err error) { return } - if err = ca.collectDependencies(); err != nil { - return - } + err = ca.collectDependencies() return } diff --git a/artifactory/commands/python/python.go b/artifactory/commands/python/python.go index 8d1afeea6..8a32a689c 100644 --- a/artifactory/commands/python/python.go +++ b/artifactory/commands/python/python.go @@ -106,7 +106,7 @@ func (pc *PythonCommand) SetPypiRepoUrlWithCredentials() error { if err != nil { return err } - pc.args = append(pc.args, python.GetPypiRemoteRegistryFlag(pc.pythonTool), rtUrl.String()) + pc.args = append(pc.args, python.GetPypiRemoteRegistryFlag(pc.pythonTool), rtUrl) return nil } diff --git a/artifactory/commands/transferconfig/utils_test.go b/artifactory/commands/transferconfig/utils_test.go index 4406a951c..4fd79e3b0 100644 --- a/artifactory/commands/transferconfig/utils_test.go +++ b/artifactory/commands/transferconfig/utils_test.go @@ -3,6 +3,7 @@ package transferconfig import ( "archive/zip" "bytes" + biutils "github.com/jfrog/build-info-go/utils" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" "golang.org/x/exp/slices" "io" @@ -43,7 +44,7 @@ func initHandleTypoInAccessBootstrapTest(t *testing.T) (exportDir string, cleanu exportDir, err := fileutils.CreateTempDir() assert.NoError(t, err) testDataPath := filepath.Join("..", "testdata", "artifactory_export") - assert.NoError(t, fileutils.CopyDir(testDataPath, exportDir, true, nil)) + assert.NoError(t, biutils.CopyDir(testDataPath, exportDir, true, nil)) cleanupFunc = func() { assert.NoError(t, fileutils.RemoveTempDir(exportDir), "Couldn't remove temp dir") } diff --git a/artifactory/commands/transferfiles/errorshandler.go b/artifactory/commands/transferfiles/errorshandler.go index f716f2a2e..f67079039 100644 --- a/artifactory/commands/transferfiles/errorshandler.go +++ b/artifactory/commands/transferfiles/errorshandler.go @@ -189,7 +189,7 @@ func (mng *TransferErrorsMng) writeErrorContent(e ExtendedFileUploadStatusRespon } func (mng *TransferErrorsMng) writeSkippedErrorContent(e ExtendedFileUploadStatusResponse) error { - log.Debug(fmt.Sprintf("write %s to file %s", e.Reason, mng.errorWriterMng.skipped.filePath)) + log.Debug(fmt.Sprintf("Writing '%s' to file %s", e.Reason, mng.errorWriterMng.skipped.filePath)) mng.errorWriterMng.skipped.writer.Write(e) mng.errorWriterMng.skipped.errorCount++ // If file contains maximum number of errors - create and write to a new errors file @@ -213,7 +213,7 @@ func (mng *TransferErrorsMng) writeSkippedErrorContent(e ExtendedFileUploadStatu } func (mng *TransferErrorsMng) writeRetryableErrorContent(e ExtendedFileUploadStatusResponse) error { - log.Debug(fmt.Sprintf("write %s to file %s", e.Reason, mng.errorWriterMng.retryable.filePath)) + log.Debug(fmt.Sprintf("Writing '%s' to file %s", e.Reason, mng.errorWriterMng.retryable.filePath)) mng.errorWriterMng.retryable.writer.Write(e) mng.errorWriterMng.retryable.errorCount++ // If file contains maximum number of errors - create and write to a new errors file diff --git a/artifactory/commands/transferinstall/datatransferinstall.go b/artifactory/commands/transferinstall/datatransferinstall.go index 0c4ac6371..8381966fe 100644 --- a/artifactory/commands/transferinstall/datatransferinstall.go +++ b/artifactory/commands/transferinstall/datatransferinstall.go @@ -2,7 +2,7 @@ package transferinstall import ( "fmt" - downloadutils "github.com/jfrog/build-info-go/utils" + biutils "github.com/jfrog/build-info-go/utils" "github.com/jfrog/gofrog/version" "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" "github.com/jfrog/jfrog-cli-core/v2/utils/config" @@ -293,7 +293,7 @@ func DownloadFiles(src string, pluginDir string, bundle PluginFiles) (err error) if err = fileutils.CreateDirIfNotExist(dstDirPath); err != nil { return } - if err = downloadutils.DownloadFile(filepath.Join(dstDirPath, fileName), srcURL); err != nil { + if err = biutils.DownloadFile(filepath.Join(dstDirPath, fileName), srcURL); err != nil { err = downloadConnectionErr(src, fileName, err.Error()) return } @@ -311,7 +311,7 @@ func CopyFiles(src string, pluginDir string, bundle PluginFiles) (err error) { if err = fileutils.CreateDirIfNotExist(dstDirPath); err != nil { return } - if err = fileutils.CopyFile(dstDirPath, srcPath); err != nil { + if err = biutils.CopyFile(dstDirPath, srcPath); err != nil { return } } diff --git a/artifactory/commands/utils/npmcmdutils.go b/artifactory/commands/utils/npmcmdutils.go index a9c3dcc7f..883fa88d2 100644 --- a/artifactory/commands/utils/npmcmdutils.go +++ b/artifactory/commands/utils/npmcmdutils.go @@ -1,14 +1,16 @@ package utils import ( + "errors" "fmt" + "io" "net/http" "os" + "path/filepath" "strings" xrutils "github.com/jfrog/jfrog-cli-core/v2/xray/utils" - "github.com/jfrog/jfrog-cli-core/v2/utils/ioutils" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" @@ -114,28 +116,51 @@ func ExtractNpmOptionsFromArgs(args []string) (detailedSummary, xrayScan bool, s // BackupFile creates a backup of the file in filePath. The backup will be found at backupPath. // The returned restore function can be called to restore the file's state - the file in filePath will be replaced by the backup in backupPath. // If there is no file at filePath, a backup file won't be created, and the restore function will delete the file at filePath. -func BackupFile(filePath, backupPath string) (restore func() error, err error) { +func BackupFile(filePath, backupFileName string) (restore func() error, err error) { fileInfo, err := os.Stat(filePath) if err != nil { if os.IsNotExist(err) { - return createRestoreFileFunc(filePath, backupPath), nil + return createRestoreFileFunc(filePath, backupFileName), nil } return nil, errorutils.CheckError(err) } - fileMode := fileInfo.Mode() - if err = ioutils.CopyFile(filePath, backupPath, fileMode); err != nil { + if err = cloneFile(filePath, backupFileName, fileInfo.Mode()); err != nil { return nil, err } - log.Debug("The file", filePath, "was backed up successfully to", backupPath) - return createRestoreFileFunc(filePath, backupPath), nil + log.Debug("The file", filePath, "was backed up successfully to", backupFileName) + return createRestoreFileFunc(filePath, backupFileName), nil +} + +func cloneFile(origFile, newName string, fileMode os.FileMode) (err error) { + from, err := os.Open(origFile) + if errorutils.CheckError(err) != nil { + return + } + defer func() { + err = errors.Join(err, from.Close()) + }() + + to, err := os.OpenFile(filepath.Join(filepath.Dir(origFile), newName), os.O_RDWR|os.O_CREATE, fileMode) + if errorutils.CheckError(err) != nil { + return + } + defer func() { + err = errors.Join(err, to.Close()) + }() + + if _, err = io.Copy(to, from); err != nil { + err = errorutils.CheckError(err) + } + return } // createRestoreFileFunc creates a function for restoring a file from its backup. // The returned function replaces the file in filePath with the backup in backupPath. // If there is no file at backupPath (which means there was no file at filePath when BackupFile() was called), then the function deletes the file at filePath. -func createRestoreFileFunc(filePath, backupPath string) func() error { +func createRestoreFileFunc(filePath, backupFileName string) func() error { return func() error { + backupPath := filepath.Join(filepath.Dir(filePath), backupFileName) if _, err := os.Stat(backupPath); err != nil { if os.IsNotExist(err) { err = os.Remove(filePath) @@ -148,7 +173,6 @@ func createRestoreFileFunc(filePath, backupPath string) func() error { return errorutils.CheckError(err) } log.Debug("Restored the file", filePath, "successfully") - return nil } } diff --git a/artifactory/commands/utils/result_test.go b/artifactory/commands/utils/result_test.go index e7c5b66e7..81423a2ff 100644 --- a/artifactory/commands/utils/result_test.go +++ b/artifactory/commands/utils/result_test.go @@ -1,6 +1,7 @@ package utils import ( + biutils "github.com/jfrog/build-info-go/utils" testsutils "github.com/jfrog/jfrog-client-go/utils/tests" "os" "path" @@ -52,7 +53,7 @@ func createTempDeployableArtifactFile() (filePath string, err error) { if err != nil { return } - err = fileutils.CopyFile(tmpDir, summary.Name()) + err = biutils.CopyFile(tmpDir, summary.Name()) if err != nil { return } diff --git a/artifactory/commands/yarn/yarn.go b/artifactory/commands/yarn/yarn.go index 2d46b6dde..01a62713c 100644 --- a/artifactory/commands/yarn/yarn.go +++ b/artifactory/commands/yarn/yarn.go @@ -95,7 +95,7 @@ func (yc *YarnCommand) Run() error { }() } - restoreYarnrcFunc, err := commandUtils.BackupFile(filepath.Join(yc.workingDirectory, YarnrcFileName), filepath.Join(yc.workingDirectory, YarnrcBackupFileName)) + restoreYarnrcFunc, err := commandUtils.BackupFile(filepath.Join(yc.workingDirectory, YarnrcFileName), YarnrcBackupFileName) if err != nil { return RestoreConfigurationsAndError(nil, restoreYarnrcFunc, err) } diff --git a/artifactory/utils/buildutils_test.go b/artifactory/utils/buildutils_test.go index cb582db1b..3147c6be3 100644 --- a/artifactory/utils/buildutils_test.go +++ b/artifactory/utils/buildutils_test.go @@ -1,6 +1,7 @@ package utils import ( + biutils "github.com/jfrog/build-info-go/utils" "os" "path/filepath" "strconv" @@ -12,7 +13,6 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" artclientutils "github.com/jfrog/jfrog-client-go/artifactory/services/utils" - "github.com/jfrog/jfrog-client-go/utils/io/fileutils" "github.com/stretchr/testify/assert" ) @@ -35,7 +35,7 @@ func TestGetBuildName(t *testing.T) { defer createTempDirCallback() confFileName := filepath.Join(tmpDir, ".jfrog", "projects") - assert.NoError(t, fileutils.CopyFile(confFileName, filepath.Join("testdata", "build.yaml"))) + assert.NoError(t, biutils.CopyFile(confFileName, filepath.Join("testdata", "build.yaml"))) wd, err := os.Getwd() assert.NoError(t, err, "Failed to get current dir") @@ -79,7 +79,7 @@ func TestGetEmptyBuildNameOnUnixAccessDenied(t *testing.T) { destConfFile := filepath.Join(tmpDir, ".jfrog", "projects") srcConfFile := filepath.Join("testdata", "build.yaml") - assert.NoError(t, fileutils.CopyFile(destConfFile, srcConfFile)) + assert.NoError(t, biutils.CopyFile(destConfFile, srcConfFile)) // Remove permissions from config file. assert.NoError(t, os.Chmod(destConfFile, 0000)) @@ -111,7 +111,7 @@ func TestGetBuildNumber(t *testing.T) { // Create build config in temp folder confFileName := filepath.Join(tmpDir, ".jfrog", "projects") - assert.NoError(t, fileutils.CopyFile(confFileName, filepath.Join("testdata", "build.yaml"))) + assert.NoError(t, biutils.CopyFile(confFileName, filepath.Join("testdata", "build.yaml"))) wd, err := os.Getwd() assert.NoError(t, err, "Failed to get current dir") @@ -217,7 +217,7 @@ func TestIsLoadedFromConfigFile(t *testing.T) { buildConfig.SetBuildName("") // Create build config in temp folder confFileName := filepath.Join(tmpDir, ".jfrog", "projects") - assert.NoError(t, fileutils.CopyFile(confFileName, filepath.Join("testdata", "build.yaml"))) + assert.NoError(t, biutils.CopyFile(confFileName, filepath.Join("testdata", "build.yaml"))) wd, err := os.Getwd() assert.NoError(t, err, "Failed to get current dir") chdirCallBack := testsutils.ChangeDirWithCallback(t, wd, tmpDir) diff --git a/artifactory/utils/dependenciesutils.go b/artifactory/utils/dependenciesutils.go index 7cfe5c4bf..790fd01fe 100644 --- a/artifactory/utils/dependenciesutils.go +++ b/artifactory/utils/dependenciesutils.go @@ -3,6 +3,7 @@ package utils import ( "errors" "fmt" + biutils "github.com/jfrog/build-info-go/utils" "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" xrayutils "github.com/jfrog/jfrog-cli-core/v2/xray/utils" @@ -224,7 +225,7 @@ func DownloadDependency(artDetails *config.ServerDetails, downloadPath, targetPa if err != nil { return err } - return fileutils.CopyDir(tempDirPath, localDir, true, nil) + return biutils.CopyDir(tempDirPath, localDir, true, nil) } func createHttpClient(artDetails *config.ServerDetails) (rtHttpClient *jfroghttpclient.JfrogHttpClient, httpClientDetails httputils.HttpClientDetails, err error) { diff --git a/buildscripts/download-jars.sh b/buildscripts/download-jars.sh index f60fb47d0..e5e6a5510 100755 --- a/buildscripts/download-jars.sh +++ b/buildscripts/download-jars.sh @@ -10,5 +10,5 @@ GRADLE_DEP_TREE_VERSION="2.2.0" MAVEN_DEP_TREE_VERSION="1.0.0" -curl -fL https://releases.jfrog.io/artifactory/oss-release-local/com/jfrog/gradle-dep-tree/${GRADLE_DEP_TREE_VERSION}/gradle-dep-tree-${GRADLE_DEP_TREE_VERSION}.jar -o xray/audit/java/gradle-dep-tree.jar -# curl -fL https://releases.jfrog.io/artifactory/oss-release-local/com/jfrog/maven-dep-tree/${MAVEN_DEP_TREE_VERSION}/maven-dep-tree-${MAVEN_DEP_TREE_VERSION}.jar -o xray/audit/java/maven-dep-tree.jar +curl -fL https://releases.jfrog.io/artifactory/oss-release-local/com/jfrog/gradle-dep-tree/${GRADLE_DEP_TREE_VERSION}/gradle-dep-tree-${GRADLE_DEP_TREE_VERSION}.jar -o xray/commands/audit/sca/java/gradle-dep-tree.jar +# curl -fL https://releases.jfrog.io/artifactory/oss-release-local/com/jfrog/maven-dep-tree/${MAVEN_DEP_TREE_VERSION}/maven-dep-tree-${MAVEN_DEP_TREE_VERSION}.jar -o xray/commands/audit/sca/java/maven-dep-tree.jar diff --git a/common/spec/distributionrules.go b/common/spec/distributionrules.go index 6a29f37f1..686fc7eb2 100644 --- a/common/spec/distributionrules.go +++ b/common/spec/distributionrules.go @@ -2,8 +2,8 @@ package spec import ( "encoding/json" + "github.com/jfrog/jfrog-client-go/utils/distribution" - "github.com/jfrog/jfrog-client-go/distribution/services/utils" "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" ) @@ -25,8 +25,8 @@ func (distributionRules *DistributionRules) Get(index int) *DistributionRule { return new(DistributionRule) } -func (distributionRule *DistributionRule) ToDistributionCommonParams() *utils.DistributionCommonParams { - return &utils.DistributionCommonParams{ +func (distributionRule *DistributionRule) ToDistributionCommonParams() *distribution.DistributionCommonParams { + return &distribution.DistributionCommonParams{ SiteName: distributionRule.SiteName, CityName: distributionRule.CityName, CountryCodes: distributionRule.CountryCodes, diff --git a/distribution/commands/deletebundle.go b/distribution/commands/deletebundle.go index 264ebcdd9..0dc9b0cc8 100644 --- a/distribution/commands/deletebundle.go +++ b/distribution/commands/deletebundle.go @@ -3,6 +3,7 @@ package commands import ( "encoding/json" "fmt" + "github.com/jfrog/jfrog-client-go/utils/distribution" "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" "github.com/jfrog/jfrog-cli-core/v2/common/spec" @@ -100,9 +101,9 @@ func (db *DeleteReleaseBundleCommand) confirmDelete(distributionRulesEmpty bool) "You can avoid this confirmation message by adding --quiet to the command.", false), nil } - var distributionRulesBodies []services.DistributionRulesBody + var distributionRulesBodies []distribution.DistributionRulesBody for _, rule := range db.deleteBundlesParams.DistributionRules { - distributionRulesBodies = append(distributionRulesBodies, services.DistributionRulesBody{ + distributionRulesBodies = append(distributionRulesBodies, distribution.DistributionRulesBody{ SiteName: rule.GetSiteName(), CityName: rule.GetCityName(), CountryCodes: rule.GetCountryCodes(), diff --git a/distribution/commands/distributebundle.go b/distribution/commands/distributebundle.go index 924dcafa0..c2a18452c 100644 --- a/distribution/commands/distributebundle.go +++ b/distribution/commands/distributebundle.go @@ -4,12 +4,12 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" "github.com/jfrog/jfrog-cli-core/v2/common/spec" "github.com/jfrog/jfrog-cli-core/v2/utils/config" - "github.com/jfrog/jfrog-client-go/distribution/services" + "github.com/jfrog/jfrog-client-go/utils/distribution" ) -type DistributeReleaseBundleCommand struct { +type DistributeReleaseBundleV1Command struct { serverDetails *config.ServerDetails - distributeBundlesParams services.DistributionParams + distributeBundlesParams distribution.DistributionParams distributionRules *spec.DistributionRules sync bool maxWaitMinutes int @@ -17,46 +17,46 @@ type DistributeReleaseBundleCommand struct { autoCreateRepo bool } -func NewReleaseBundleDistributeCommand() *DistributeReleaseBundleCommand { - return &DistributeReleaseBundleCommand{} +func NewReleaseBundleDistributeV1Command() *DistributeReleaseBundleV1Command { + return &DistributeReleaseBundleV1Command{} } -func (db *DistributeReleaseBundleCommand) SetServerDetails(serverDetails *config.ServerDetails) *DistributeReleaseBundleCommand { +func (db *DistributeReleaseBundleV1Command) SetServerDetails(serverDetails *config.ServerDetails) *DistributeReleaseBundleV1Command { db.serverDetails = serverDetails return db } -func (db *DistributeReleaseBundleCommand) SetDistributeBundleParams(params services.DistributionParams) *DistributeReleaseBundleCommand { +func (db *DistributeReleaseBundleV1Command) SetDistributeBundleParams(params distribution.DistributionParams) *DistributeReleaseBundleV1Command { db.distributeBundlesParams = params return db } -func (db *DistributeReleaseBundleCommand) SetDistributionRules(distributionRules *spec.DistributionRules) *DistributeReleaseBundleCommand { +func (db *DistributeReleaseBundleV1Command) SetDistributionRules(distributionRules *spec.DistributionRules) *DistributeReleaseBundleV1Command { db.distributionRules = distributionRules return db } -func (db *DistributeReleaseBundleCommand) SetSync(sync bool) *DistributeReleaseBundleCommand { +func (db *DistributeReleaseBundleV1Command) SetSync(sync bool) *DistributeReleaseBundleV1Command { db.sync = sync return db } -func (db *DistributeReleaseBundleCommand) SetMaxWaitMinutes(maxWaitMinutes int) *DistributeReleaseBundleCommand { +func (db *DistributeReleaseBundleV1Command) SetMaxWaitMinutes(maxWaitMinutes int) *DistributeReleaseBundleV1Command { db.maxWaitMinutes = maxWaitMinutes return db } -func (db *DistributeReleaseBundleCommand) SetDryRun(dryRun bool) *DistributeReleaseBundleCommand { +func (db *DistributeReleaseBundleV1Command) SetDryRun(dryRun bool) *DistributeReleaseBundleV1Command { db.dryRun = dryRun return db } -func (db *DistributeReleaseBundleCommand) SetAutoCreateRepo(autoCreateRepo bool) *DistributeReleaseBundleCommand { +func (db *DistributeReleaseBundleV1Command) SetAutoCreateRepo(autoCreateRepo bool) *DistributeReleaseBundleV1Command { db.autoCreateRepo = autoCreateRepo return db } -func (db *DistributeReleaseBundleCommand) Run() error { +func (db *DistributeReleaseBundleV1Command) Run() error { servicesManager, err := utils.CreateDistributionServiceManager(db.serverDetails, db.dryRun) if err != nil { return err @@ -72,10 +72,10 @@ func (db *DistributeReleaseBundleCommand) Run() error { return servicesManager.DistributeReleaseBundle(db.distributeBundlesParams, db.autoCreateRepo) } -func (db *DistributeReleaseBundleCommand) ServerDetails() (*config.ServerDetails, error) { +func (db *DistributeReleaseBundleV1Command) ServerDetails() (*config.ServerDetails, error) { return db.serverDetails, nil } -func (db *DistributeReleaseBundleCommand) CommandName() string { +func (db *DistributeReleaseBundleV1Command) CommandName() string { return "rt_distribute_bundle" } diff --git a/go.mod b/go.mod index 17e82c146..8ed9ccd71 100644 --- a/go.mod +++ b/go.mod @@ -2,17 +2,19 @@ module github.com/jfrog/jfrog-cli-core/v2 go 1.20 +require github.com/c-bata/go-prompt v0.2.5 // Should not be updated to 0.2.6 due to a bug (https://github.com/jfrog/jfrog-cli-core/pull/372) + require ( github.com/buger/jsonparser v1.1.1 github.com/chzyer/readline v1.5.1 github.com/forPelevin/gomoji v1.1.8 github.com/gocarina/gocsv v0.0.0-20230616125104-99d496ca653d - github.com/google/uuid v1.3.0 + github.com/google/uuid v1.3.1 github.com/gookit/color v1.5.4 - github.com/jedib0t/go-pretty/v6 v6.4.6 - github.com/jfrog/build-info-go v1.9.9 + github.com/jedib0t/go-pretty/v6 v6.4.7 + github.com/jfrog/build-info-go v1.9.10 github.com/jfrog/gofrog v1.3.0 - github.com/jfrog/jfrog-client-go v1.31.6 + github.com/jfrog/jfrog-client-go v1.32.1 github.com/magiconair/properties v1.8.7 github.com/manifoldco/promptui v0.9.0 github.com/owenrumney/go-sarif/v2 v2.2.0 @@ -21,26 +23,24 @@ require ( github.com/stretchr/testify v1.8.4 github.com/urfave/cli v1.22.14 github.com/vbauerster/mpb/v7 v7.5.3 - golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 golang.org/x/mod v0.12.0 golang.org/x/sync v0.3.0 - golang.org/x/term v0.11.0 - golang.org/x/text v0.12.0 + golang.org/x/term v0.12.0 + golang.org/x/text v0.13.0 gopkg.in/yaml.v3 v3.0.1 ) -require github.com/c-bata/go-prompt v0.2.5 // Should not be updated to 0.2.6 due to a bug (https://github.com/jfrog/jfrog-cli-core/pull/372) - require ( dario.cat/mergo v1.0.0 // indirect github.com/BurntSushi/toml v1.3.2 // indirect - github.com/CycloneDX/cyclonedx-go v0.7.1 // indirect + github.com/CycloneDX/cyclonedx-go v0.7.2 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect github.com/VividCortex/ewma v1.2.0 // indirect github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect github.com/acomagu/bufpipe v1.0.4 // indirect - github.com/andybalholm/brotli v1.0.5 // indirect + github.com/andybalholm/brotli v1.0.1 // indirect github.com/cloudflare/circl v1.3.3 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -52,15 +52,15 @@ require ( github.com/go-git/go-git/v5 v5.8.1 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/snappy v0.0.4 // indirect + github.com/golang/snappy v0.0.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect - github.com/klauspost/compress v1.15.9 // indirect + github.com/klauspost/compress v1.11.4 // indirect github.com/klauspost/cpuid/v2 v2.2.3 // indirect github.com/klauspost/pgzip v1.2.5 // indirect github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect github.com/mattn/go-tty v0.0.3 // indirect github.com/mholt/archiver/v3 v3.5.1 // indirect @@ -68,7 +68,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/nwaples/rardecode v1.1.0 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect - github.com/pierrec/lz4/v4 v4.1.15 // indirect + github.com/pierrec/lz4/v4 v4.1.2 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pkg/term v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -85,16 +85,14 @@ require ( github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect - golang.org/x/crypto v0.12.0 // indirect - golang.org/x/net v0.14.0 // indirect; indirectmake - golang.org/x/sys v0.11.0 // indirect - golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect + golang.org/x/crypto v0.13.0 // indirect + golang.org/x/net v0.15.0 // indirect + golang.org/x/sys v0.12.0 // indirect + golang.org/x/tools v0.13.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) -// replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go v1.28.1-0.20230803140217-0a5f43783ae8 - -// replace github.com/jfrog/build-info-go => github.com/jfrog/build-info-go v1.8.9-0.20230828134416-f0db33dd9344 +// replace github.com/jfrog/build-info-go => github.com/jfrog/build-info-go v1.8.9-0.20230905120411-62d1bdd4eb38 // replace github.com/jfrog/gofrog => github.com/jfrog/gofrog v1.2.6-0.20230418122323-2bf299dd6d27 diff --git a/go.sum b/go.sum index d179d21e3..c0825b56f 100644 --- a/go.sum +++ b/go.sum @@ -42,22 +42,21 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/CycloneDX/cyclonedx-go v0.7.1 h1:5w1SxjGm9MTMNTuRbEPyw21ObdbaagTWF/KfF0qHTRE= -github.com/CycloneDX/cyclonedx-go v0.7.1/go.mod h1:N/nrdWQI2SIjaACyyDs/u7+ddCkyl/zkNs8xFsHF2Ps= +github.com/CycloneDX/cyclonedx-go v0.7.2 h1:kKQ0t1dPOlugSIYVOMiMtFqeXI2wp/f5DBIdfux8gnQ= +github.com/CycloneDX/cyclonedx-go v0.7.2/go.mod h1:K2bA+324+Og0X84fA8HhN2X066K7Bxz4rpMQ4ZhjtSk= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 h1:KLq8BE0KwCL+mmXnjLWEAOYO+2l2AE4YMmqG1ZpZHBs= -github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg= +github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= +github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc= github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= -github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= -github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= @@ -148,9 +147,8 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw= github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -179,8 +177,8 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= @@ -194,23 +192,22 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jedib0t/go-pretty/v6 v6.4.6 h1:v6aG9h6Uby3IusSSEjHaZNXpHFhzqMmjXcPq1Rjl9Jw= -github.com/jedib0t/go-pretty/v6 v6.4.6/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs= -github.com/jfrog/build-info-go v1.9.9 h1:YMA9okHawBNL8SrCWzqULSf5M4W+YnWyUhmkWSjoXEE= -github.com/jfrog/build-info-go v1.9.9/go.mod h1:t31QRpH5xUJKw8XkQlAA+Aq7aanyS1rrzpcK8xSNVts= +github.com/jedib0t/go-pretty/v6 v6.4.7 h1:lwiTJr1DEkAgzljsUsORmWsVn5MQjt1BPJdPCtJ6KXE= +github.com/jedib0t/go-pretty/v6 v6.4.7/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs= +github.com/jfrog/build-info-go v1.9.10 h1:uXnDLVxpqxoAMpXcki00QaBB+M2BoGMMpHODPkmmYOY= +github.com/jfrog/build-info-go v1.9.10/go.mod h1:ujJ8XQZMdT2tMkLSMJNyDd1pCY+duwHdjV+9or9FLIg= github.com/jfrog/gofrog v1.3.0 h1:o4zgsBZE4QyDbz2M7D4K6fXPTBJht+8lE87mS9bw7Gk= github.com/jfrog/gofrog v1.3.0/go.mod h1:IFMc+V/yf7rA5WZ74CSbXe+Lgf0iApEQLxRZVzKRUR0= -github.com/jfrog/jfrog-client-go v1.31.6 h1:uWuyT4BDm9s5ES6oDTBny9Gl6yf8iKFjcbmHSHQZrDc= -github.com/jfrog/jfrog-client-go v1.31.6/go.mod h1:icb00ZJN/mMMNkQduHDkzpqsXH9Flwi3f3COYexq3Nc= +github.com/jfrog/jfrog-client-go v1.32.1 h1:RQmuPSLsF5222vZJzwkgHSZMMJF83ExS7SwIvh4P+H8= +github.com/jfrog/jfrog-client-go v1.32.1/go.mod h1:362+oa7uTTYurzBs1L0dmUTlLo7uhpAU/pwM5Zb9clg= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.11.4 h1:kz40R/YWls3iqT9zX9AHN3WoVsrAWVyui5sxuLqiXqU= github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY= -github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU= github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= @@ -237,9 +234,8 @@ github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= @@ -260,9 +256,8 @@ github.com/owenrumney/go-sarif/v2 v2.2.0 h1:1DmZaijK0HBZCR1fgcDSGa7VzYkU9NDmbZ7q github.com/owenrumney/go-sarif/v2 v2.2.0/go.mod h1:MSqMMx9WqlBSY7pXoOZWgEsVB4FDNfhcaXDA1j6Sr+w= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pierrec/lz4/v4 v4.1.2 h1:qvY3YFXRQE/XB8MlLzJH7mSzBs74eA2gg52YTk6jUPM= github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0= -github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= @@ -313,6 +308,7 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/terminalstatic/go-xsd-validate v0.1.5 h1:RqpJnf6HGE2CB/lZB1A8BYguk8uRtcvYAPLCF15qguo= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I= github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= @@ -324,6 +320,9 @@ github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+ github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= @@ -351,8 +350,8 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -363,8 +362,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= -golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -428,8 +427,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -505,7 +504,6 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220909162455-aba9fc2a8ff2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -513,15 +511,15 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -534,8 +532,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -588,8 +586,8 @@ golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 h1:Vve/L0v7CXXuxUmaMGIEK/dEeq7uiqb5qBgQrZzIE7E= -golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/lifecycle/common.go b/lifecycle/common.go index 93045de66..55e7ef771 100644 --- a/lifecycle/common.go +++ b/lifecycle/common.go @@ -5,8 +5,11 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-client-go/lifecycle" "github.com/jfrog/jfrog-client-go/lifecycle/services" + clientUtils "github.com/jfrog/jfrog-client-go/utils" ) +const minimalLifecycleArtifactoryVersion = "7.63.2" + type releaseBundleCmd struct { serverDetails *config.ServerDetails releaseBundleName string @@ -34,3 +37,17 @@ func (rbc *releaseBundleCmd) getPrerequisites() (servicesManager *lifecycle.Life } return } + +func validateArtifactoryVersionSupported(serverDetails *config.ServerDetails) error { + rtServiceManager, err := utils.CreateServiceManager(serverDetails, 3, 0, false) + if err != nil { + return err + } + + versionStr, err := rtServiceManager.GetVersion() + if err != nil { + return err + } + + return clientUtils.ValidateMinimumVersion(clientUtils.Artifactory, versionStr, minimalLifecycleArtifactoryVersion) +} diff --git a/lifecycle/createcommon.go b/lifecycle/createcommon.go index aab498eec..5149a82a7 100644 --- a/lifecycle/createcommon.go +++ b/lifecycle/createcommon.go @@ -4,65 +4,69 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/utils/config" ) -type ReleaseBundleCreate struct { +type ReleaseBundleCreateCommand struct { releaseBundleCmd buildsSpecPath string releaseBundlesSpecPath string } -func NewReleaseBundleCreate() *ReleaseBundleCreate { - return &ReleaseBundleCreate{} +func NewReleaseBundleCreateCommand() *ReleaseBundleCreateCommand { + return &ReleaseBundleCreateCommand{} } -func (rbc *ReleaseBundleCreate) SetServerDetails(serverDetails *config.ServerDetails) *ReleaseBundleCreate { +func (rbc *ReleaseBundleCreateCommand) SetServerDetails(serverDetails *config.ServerDetails) *ReleaseBundleCreateCommand { rbc.serverDetails = serverDetails return rbc } -func (rbc *ReleaseBundleCreate) SetReleaseBundleName(releaseBundleName string) *ReleaseBundleCreate { +func (rbc *ReleaseBundleCreateCommand) SetReleaseBundleName(releaseBundleName string) *ReleaseBundleCreateCommand { rbc.releaseBundleName = releaseBundleName return rbc } -func (rbc *ReleaseBundleCreate) SetReleaseBundleVersion(releaseBundleVersion string) *ReleaseBundleCreate { +func (rbc *ReleaseBundleCreateCommand) SetReleaseBundleVersion(releaseBundleVersion string) *ReleaseBundleCreateCommand { rbc.releaseBundleVersion = releaseBundleVersion return rbc } -func (rbc *ReleaseBundleCreate) SetSigningKeyName(signingKeyName string) *ReleaseBundleCreate { +func (rbc *ReleaseBundleCreateCommand) SetSigningKeyName(signingKeyName string) *ReleaseBundleCreateCommand { rbc.signingKeyName = signingKeyName return rbc } -func (rbc *ReleaseBundleCreate) SetSync(sync bool) *ReleaseBundleCreate { +func (rbc *ReleaseBundleCreateCommand) SetSync(sync bool) *ReleaseBundleCreateCommand { rbc.sync = sync return rbc } -func (rbc *ReleaseBundleCreate) SetReleaseBundleProject(rbProjectKey string) *ReleaseBundleCreate { +func (rbc *ReleaseBundleCreateCommand) SetReleaseBundleProject(rbProjectKey string) *ReleaseBundleCreateCommand { rbc.rbProjectKey = rbProjectKey return rbc } -func (rbc *ReleaseBundleCreate) SetBuildsSpecPath(buildsSpecPath string) *ReleaseBundleCreate { +func (rbc *ReleaseBundleCreateCommand) SetBuildsSpecPath(buildsSpecPath string) *ReleaseBundleCreateCommand { rbc.buildsSpecPath = buildsSpecPath return rbc } -func (rbc *ReleaseBundleCreate) SetReleaseBundlesSpecPath(releaseBundlesSpecPath string) *ReleaseBundleCreate { +func (rbc *ReleaseBundleCreateCommand) SetReleaseBundlesSpecPath(releaseBundlesSpecPath string) *ReleaseBundleCreateCommand { rbc.releaseBundlesSpecPath = releaseBundlesSpecPath return rbc } -func (rbc *ReleaseBundleCreate) CommandName() string { +func (rbc *ReleaseBundleCreateCommand) CommandName() string { return "rb_create" } -func (rbc *ReleaseBundleCreate) ServerDetails() (*config.ServerDetails, error) { +func (rbc *ReleaseBundleCreateCommand) ServerDetails() (*config.ServerDetails, error) { return rbc.serverDetails, nil } -func (rbc *ReleaseBundleCreate) Run() error { +func (rbc *ReleaseBundleCreateCommand) Run() error { + if err := validateArtifactoryVersionSupported(rbc.serverDetails); err != nil { + return err + } + servicesManager, rbDetails, params, err := rbc.getPrerequisites() if err != nil { return err diff --git a/lifecycle/createfrombuilds.go b/lifecycle/createfrombuilds.go index f54ea89f5..f9a13ed2e 100644 --- a/lifecycle/createfrombuilds.go +++ b/lifecycle/createfrombuilds.go @@ -11,7 +11,7 @@ import ( "github.com/jfrog/jfrog-client-go/utils/io/fileutils" ) -func (rbc *ReleaseBundleCreate) createFromBuilds(servicesManager *lifecycle.LifecycleServicesManager, +func (rbc *ReleaseBundleCreateCommand) createFromBuilds(servicesManager *lifecycle.LifecycleServicesManager, rbDetails services.ReleaseBundleDetails, params services.CreateOrPromoteReleaseBundleParams) error { builds := CreateFromBuildsSpec{} @@ -34,7 +34,7 @@ func (rbc *ReleaseBundleCreate) createFromBuilds(servicesManager *lifecycle.Life return servicesManager.CreateReleaseBundleFromBuilds(rbDetails, params, buildsSource) } -func (rbc *ReleaseBundleCreate) convertToBuildsSource(builds CreateFromBuildsSpec) (services.CreateFromBuildsSource, error) { +func (rbc *ReleaseBundleCreateCommand) convertToBuildsSource(builds CreateFromBuildsSpec) (services.CreateFromBuildsSource, error) { buildsSource := services.CreateFromBuildsSource{} for _, build := range builds.Builds { buildSource := services.BuildSource{BuildName: build.Name} @@ -49,7 +49,7 @@ func (rbc *ReleaseBundleCreate) convertToBuildsSource(builds CreateFromBuildsSpe return buildsSource, nil } -func (rbc *ReleaseBundleCreate) getLatestBuildNumberIfEmpty(buildName, buildNumber, project string) (string, error) { +func (rbc *ReleaseBundleCreateCommand) getLatestBuildNumberIfEmpty(buildName, buildNumber, project string) (string, error) { if buildNumber != "" { return buildNumber, nil } @@ -69,7 +69,7 @@ func (rbc *ReleaseBundleCreate) getLatestBuildNumberIfEmpty(buildName, buildNumb return buildNumber, nil } -func (rbc *ReleaseBundleCreate) getAqlService() (*rtServices.AqlService, error) { +func (rbc *ReleaseBundleCreateCommand) getAqlService() (*rtServices.AqlService, error) { rtServiceManager, err := rtUtils.CreateServiceManager(rbc.serverDetails, 3, 0, false) if err != nil { return nil, err diff --git a/lifecycle/createfrombundles.go b/lifecycle/createfrombundles.go index 729babaf3..872d76765 100644 --- a/lifecycle/createfrombundles.go +++ b/lifecycle/createfrombundles.go @@ -8,7 +8,7 @@ import ( "github.com/jfrog/jfrog-client-go/utils/io/fileutils" ) -func (rbc *ReleaseBundleCreate) createFromReleaseBundles(servicesManager *lifecycle.LifecycleServicesManager, +func (rbc *ReleaseBundleCreateCommand) createFromReleaseBundles(servicesManager *lifecycle.LifecycleServicesManager, rbDetails services.ReleaseBundleDetails, params services.CreateOrPromoteReleaseBundleParams) error { bundles := CreateFromReleaseBundlesSpec{} @@ -28,7 +28,7 @@ func (rbc *ReleaseBundleCreate) createFromReleaseBundles(servicesManager *lifecy return servicesManager.CreateReleaseBundleFromBundles(rbDetails, params, releaseBundlesSource) } -func (rbc *ReleaseBundleCreate) convertToReleaseBundlesSource(bundles CreateFromReleaseBundlesSpec) services.CreateFromReleaseBundlesSource { +func (rbc *ReleaseBundleCreateCommand) convertToReleaseBundlesSource(bundles CreateFromReleaseBundlesSpec) services.CreateFromReleaseBundlesSource { releaseBundlesSource := services.CreateFromReleaseBundlesSource{} for _, rb := range bundles.ReleaseBundles { rbSource := services.ReleaseBundleSource{ diff --git a/lifecycle/distribute.go b/lifecycle/distribute.go new file mode 100644 index 000000000..b3d5ea698 --- /dev/null +++ b/lifecycle/distribute.go @@ -0,0 +1,88 @@ +package lifecycle + +import ( + "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" + "github.com/jfrog/jfrog-cli-core/v2/common/spec" + "github.com/jfrog/jfrog-cli-core/v2/utils/config" + "github.com/jfrog/jfrog-client-go/lifecycle/services" + "github.com/jfrog/jfrog-client-go/utils/distribution" +) + +type ReleaseBundleDistributeCommand struct { + serverDetails *config.ServerDetails + distributeBundlesParams distribution.DistributionParams + distributionRules *spec.DistributionRules + dryRun bool + autoCreateRepo bool + pathMappingPattern string + pathMappingTarget string +} + +func NewReleaseBundleDistributeCommand() *ReleaseBundleDistributeCommand { + return &ReleaseBundleDistributeCommand{} +} + +func (rbd *ReleaseBundleDistributeCommand) SetServerDetails(serverDetails *config.ServerDetails) *ReleaseBundleDistributeCommand { + rbd.serverDetails = serverDetails + return rbd +} + +func (rbd *ReleaseBundleDistributeCommand) SetDistributeBundleParams(params distribution.DistributionParams) *ReleaseBundleDistributeCommand { + rbd.distributeBundlesParams = params + return rbd +} + +func (rbd *ReleaseBundleDistributeCommand) SetDistributionRules(distributionRules *spec.DistributionRules) *ReleaseBundleDistributeCommand { + rbd.distributionRules = distributionRules + return rbd +} + +func (rbd *ReleaseBundleDistributeCommand) SetDryRun(dryRun bool) *ReleaseBundleDistributeCommand { + rbd.dryRun = dryRun + return rbd +} + +func (rbd *ReleaseBundleDistributeCommand) SetAutoCreateRepo(autoCreateRepo bool) *ReleaseBundleDistributeCommand { + rbd.autoCreateRepo = autoCreateRepo + return rbd +} + +func (rbd *ReleaseBundleDistributeCommand) SetPathMappingPattern(pathMappingPattern string) *ReleaseBundleDistributeCommand { + rbd.pathMappingPattern = pathMappingPattern + return rbd +} + +func (rbd *ReleaseBundleDistributeCommand) SetPathMappingTarget(pathMappingTarget string) *ReleaseBundleDistributeCommand { + rbd.pathMappingTarget = pathMappingTarget + return rbd +} + +func (rbd *ReleaseBundleDistributeCommand) Run() error { + if err := validateArtifactoryVersionSupported(rbd.serverDetails); err != nil { + return err + } + + servicesManager, err := utils.CreateLifecycleServiceManager(rbd.serverDetails, rbd.dryRun) + if err != nil { + return err + } + + for _, rule := range rbd.distributionRules.DistributionRules { + rbd.distributeBundlesParams.DistributionRules = append(rbd.distributeBundlesParams.DistributionRules, rule.ToDistributionCommonParams()) + } + + pathMapping := services.PathMapping{ + Pattern: rbd.pathMappingPattern, + Target: rbd.pathMappingTarget, + } + + return servicesManager.DistributeReleaseBundle(rbd.distributeBundlesParams, rbd.autoCreateRepo, pathMapping) +} + +func (rbd *ReleaseBundleDistributeCommand) ServerDetails() (*config.ServerDetails, error) { + return rbd.serverDetails, nil +} + +func (rbd *ReleaseBundleDistributeCommand) CommandName() string { + return "rb_distribute" +} diff --git a/lifecycle/promote.go b/lifecycle/promote.go index ac7921715..6958bae5a 100644 --- a/lifecycle/promote.go +++ b/lifecycle/promote.go @@ -7,65 +7,69 @@ import ( "github.com/jfrog/jfrog-client-go/utils/log" ) -type ReleaseBundlePromote struct { +type ReleaseBundlePromoteCommand struct { releaseBundleCmd environment string overwrite bool } -func NewReleaseBundlePromote() *ReleaseBundlePromote { - return &ReleaseBundlePromote{} +func NewReleaseBundlePromoteCommand() *ReleaseBundlePromoteCommand { + return &ReleaseBundlePromoteCommand{} } -func (rbp *ReleaseBundlePromote) SetServerDetails(serverDetails *config.ServerDetails) *ReleaseBundlePromote { +func (rbp *ReleaseBundlePromoteCommand) SetServerDetails(serverDetails *config.ServerDetails) *ReleaseBundlePromoteCommand { rbp.serverDetails = serverDetails return rbp } -func (rbp *ReleaseBundlePromote) SetReleaseBundleName(releaseBundleName string) *ReleaseBundlePromote { +func (rbp *ReleaseBundlePromoteCommand) SetReleaseBundleName(releaseBundleName string) *ReleaseBundlePromoteCommand { rbp.releaseBundleName = releaseBundleName return rbp } -func (rbp *ReleaseBundlePromote) SetReleaseBundleVersion(releaseBundleVersion string) *ReleaseBundlePromote { +func (rbp *ReleaseBundlePromoteCommand) SetReleaseBundleVersion(releaseBundleVersion string) *ReleaseBundlePromoteCommand { rbp.releaseBundleVersion = releaseBundleVersion return rbp } -func (rbp *ReleaseBundlePromote) SetSigningKeyName(signingKeyName string) *ReleaseBundlePromote { +func (rbp *ReleaseBundlePromoteCommand) SetSigningKeyName(signingKeyName string) *ReleaseBundlePromoteCommand { rbp.signingKeyName = signingKeyName return rbp } -func (rbp *ReleaseBundlePromote) SetSync(sync bool) *ReleaseBundlePromote { +func (rbp *ReleaseBundlePromoteCommand) SetSync(sync bool) *ReleaseBundlePromoteCommand { rbp.sync = sync return rbp } -func (rbp *ReleaseBundlePromote) SetReleaseBundleProject(rbProjectKey string) *ReleaseBundlePromote { +func (rbp *ReleaseBundlePromoteCommand) SetReleaseBundleProject(rbProjectKey string) *ReleaseBundlePromoteCommand { rbp.rbProjectKey = rbProjectKey return rbp } -func (rbp *ReleaseBundlePromote) SetEnvironment(environment string) *ReleaseBundlePromote { +func (rbp *ReleaseBundlePromoteCommand) SetEnvironment(environment string) *ReleaseBundlePromoteCommand { rbp.environment = environment return rbp } -func (rbp *ReleaseBundlePromote) SetOverwrite(overwrite bool) *ReleaseBundlePromote { +func (rbp *ReleaseBundlePromoteCommand) SetOverwrite(overwrite bool) *ReleaseBundlePromoteCommand { rbp.overwrite = overwrite return rbp } -func (rbp *ReleaseBundlePromote) CommandName() string { +func (rbp *ReleaseBundlePromoteCommand) CommandName() string { return "rb_promote" } -func (rbp *ReleaseBundlePromote) ServerDetails() (*config.ServerDetails, error) { +func (rbp *ReleaseBundlePromoteCommand) ServerDetails() (*config.ServerDetails, error) { return rbp.serverDetails, nil } -func (rbp *ReleaseBundlePromote) Run() error { +func (rbp *ReleaseBundlePromoteCommand) Run() error { + if err := validateArtifactoryVersionSupported(rbp.serverDetails); err != nil { + return err + } + servicesManager, rbDetails, params, err := rbp.getPrerequisites() if err != nil { return err diff --git a/utils/config/config.go b/utils/config/config.go index 019f979b6..d774bc1b4 100644 --- a/utils/config/config.go +++ b/utils/config/config.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "github.com/buger/jsonparser" + biutils "github.com/jfrog/build-info-go/utils" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" cliLog "github.com/jfrog/jfrog-cli-core/v2/utils/log" accessAuth "github.com/jfrog/jfrog-client-go/access/auth" @@ -72,8 +73,14 @@ func GetSpecificConfig(serverId string, defaultOrEmpty bool, excludeRefreshableT return details, nil } -// Disables refreshable tokens if set in details. +// Disables the refreshable tokens mechanism if set in details. +// We identify the refreshable tokens mechanism by having both conditions: +// 1. Non-empty username and password +// 2. Non-empty access and refresh token OR token refresh interval enabled func excludeRefreshableTokensFromDetails(details *ServerDetails) { + if details.WebLogin || details.User == "" || details.Password == "" { + return + } if details.AccessToken != "" && details.ArtifactoryRefreshToken != "" || details.AccessToken != "" && details.RefreshToken != "" { details.AccessToken = "" @@ -390,7 +397,7 @@ func createHomeDirBackup() error { curBackupPath := filepath.Join(backupDir, backupName) log.Debug("Creating a homedir backup at: " + curBackupPath) exclude := []string{coreutils.JfrogBackupDirName, coreutils.JfrogDependenciesDirName, coreutils.JfrogLocksDirName, coreutils.JfrogLogsDirName} - return fileutils.CopyDir(homeDir, curBackupPath, true, exclude) + return biutils.CopyDir(homeDir, curBackupPath, true, exclude) } // Version key doesn't exist in version 0 diff --git a/utils/config/tests/utils.go b/utils/config/tests/utils.go index 0a62cf8ba..1bde9e610 100644 --- a/utils/config/tests/utils.go +++ b/utils/config/tests/utils.go @@ -1,8 +1,8 @@ package tests import ( + biutils "github.com/jfrog/build-info-go/utils" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/jfrog/jfrog-client-go/utils/io/fileutils" testsutils "github.com/jfrog/jfrog-client-go/utils/tests" "github.com/stretchr/testify/assert" "os" @@ -29,5 +29,5 @@ func CreateTempEnv(t *testing.T, copyEncryptionKey bool) (cleanUp func()) { } func copyResources(t *testing.T, sourcePath string, destPath string) { - assert.NoError(t, fileutils.CopyDir(sourcePath, destPath, true, nil)) + assert.NoError(t, biutils.CopyDir(sourcePath, destPath, true, nil)) } diff --git a/utils/coreutils/techutils.go b/utils/coreutils/techutils.go index 985c1fad3..bba09ba3f 100644 --- a/utils/coreutils/techutils.go +++ b/utils/coreutils/techutils.go @@ -1,6 +1,10 @@ package coreutils import ( + "fmt" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + "github.com/jfrog/jfrog-client-go/utils/log" + "os" "strings" "golang.org/x/text/cases" @@ -112,6 +116,11 @@ var technologiesData = map[Technology]TechData{ Nuget: { indicators: []string{".sln", ".csproj"}, formal: "NuGet", + // .NET CLI is used for NuGet projects + execCommand: "dotnet", + packageInstallationCommand: "add", + // packageName -v packageVersion + packageVersionOperator: " -v ", }, Dotnet: { indicators: []string{".sln", ".csproj"}, @@ -155,7 +164,7 @@ func (tech Technology) IsCiSetup() bool { return technologiesData[tech].ciSetupSupport } -func (tech Technology) GetPackageOperator() string { +func (tech Technology) GetPackageVersionOperator() string { return technologiesData[tech].packageVersionOperator } @@ -167,6 +176,23 @@ func (tech Technology) ApplicabilityScannable() bool { return technologiesData[tech].applicabilityScannable } +func DetectedTechnologiesList() (technologies []string) { + wd, err := os.Getwd() + if errorutils.CheckError(err) != nil { + return + } + detectedTechnologies, err := DetectTechnologies(wd, false, false) + if err != nil { + return + } + if len(detectedTechnologies) == 0 { + return + } + techStringsList := DetectedTechnologiesToSlice(detectedTechnologies) + log.Info(fmt.Sprintf("Detected: %s.", strings.Join(techStringsList, ","))) + return techStringsList +} + // DetectTechnologies tries to detect all technologies types according to the files in the given path. // 'isCiSetup' will limit the search of possible techs to Maven, Gradle, and npm. // 'recursive' will determine if the search will be limited to files in the root path or not. diff --git a/utils/coreutils/utils.go b/utils/coreutils/utils.go index f1c2b7fb1..15bb5809f 100644 --- a/utils/coreutils/utils.go +++ b/utils/coreutils/utils.go @@ -263,6 +263,27 @@ func GetWorkingDirectory() (string, error) { return currentDir, nil } +// Receives a list of relative path working dirs, returns a list of full paths working dirs +func GetFullPathsWorkingDirs(workingDirs []string) ([]string, error) { + if len(workingDirs) == 0 { + currentDir, err := GetWorkingDirectory() + if err != nil { + return nil, err + } + return []string{currentDir}, nil + } + + var fullPathsWorkingDirs []string + for _, wd := range workingDirs { + fullPathWd, err := filepath.Abs(wd) + if err != nil { + return nil, err + } + fullPathsWorkingDirs = append(fullPathsWorkingDirs, fullPathWd) + } + return fullPathsWorkingDirs, nil +} + type Credentials interface { SetUser(string) SetPassword(string) @@ -563,3 +584,18 @@ func GetServerIdAndRepo(remoteEnv string) (serverID string, repoName string, err } return } + +func GetMaskedCommandString(cmd *exec.Cmd) string { + cmdString := strings.Join(cmd.Args, " ") + // Mask url if required + matchedResult := regexp.MustCompile(utils.CredentialsInUrlRegexp).FindString(cmdString) + if matchedResult != "" { + cmdString = strings.ReplaceAll(cmdString, matchedResult, "***@") + } + + matchedResults := regexp.MustCompile(`--(?:password|access-token)=(\S+)`).FindStringSubmatch(cmdString) + if len(matchedResults) > 1 && matchedResults[1] != "" { + cmdString = strings.ReplaceAll(cmdString, matchedResults[1], "***") + } + return cmdString +} diff --git a/utils/coreutils/utils_test.go b/utils/coreutils/utils_test.go index 85469dfd6..8ddffbee4 100644 --- a/utils/coreutils/utils_test.go +++ b/utils/coreutils/utils_test.go @@ -6,6 +6,8 @@ import ( "errors" "fmt" "os" + "os/exec" + "path/filepath" "reflect" "testing" @@ -230,3 +232,50 @@ func TestSplitRepoAndServerId(t *testing.T) { }() } } + +func TestGetFullPathsWorkingDirs(t *testing.T) { + currentDir, err := GetWorkingDirectory() + assert.NoError(t, err) + dir1, err := filepath.Abs("dir1") + assert.NoError(t, err) + dir2, err := filepath.Abs("dir2") + assert.NoError(t, err) + tests := []struct { + name string + workingDirs []string + expectedDirs []string + }{ + { + name: "EmptyWorkingDirs", + workingDirs: []string{}, + expectedDirs: []string{currentDir}, + }, + { + name: "ValidWorkingDirs", + workingDirs: []string{"dir1", "dir2"}, + expectedDirs: []string{dir1, dir2}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + actualDirs, err := GetFullPathsWorkingDirs(test.workingDirs) + assert.NoError(t, err) + assert.Equal(t, test.expectedDirs, actualDirs, "Incorrect full paths of working directories") + }) + } +} + +func TestGetMaskedCommandString(t *testing.T) { + assert.Equal(t, + "pip -i ***@someurl.com/repo", + GetMaskedCommandString(exec.Command("pip", "-i", "https://user:pass@someurl.com/repo"))) + + assert.Equal(t, + "pip -i ***@someurl.com/repo --password=***", + GetMaskedCommandString(exec.Command("pip", "-i", "https://user:pass@someurl.com/repo", "--password=123"))) + + assert.Equal(t, + "pip -i ***@someurl.com/repo --access-token=***", + GetMaskedCommandString(exec.Command("pip", "-i", "https://user:pass@someurl.com/repo", "--access-token=123"))) +} diff --git a/utils/ioutils/ioutils.go b/utils/ioutils/ioutils.go index 05ca35d97..5978302d2 100644 --- a/utils/ioutils/ioutils.go +++ b/utils/ioutils/ioutils.go @@ -7,7 +7,6 @@ import ( "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/log" "golang.org/x/term" - "io" "os" "strings" "syscall" @@ -66,36 +65,6 @@ func ScanFromConsole(caption string, scanInto *string, defaultValue string) { *scanInto = strings.TrimSpace(*scanInto) } -func CopyFile(src, dst string, fileMode os.FileMode) (err error) { - from, err := os.Open(src) - if err != nil { - return errorutils.CheckError(err) - } - defer func() { - e := from.Close() - if err == nil { - err = e - } - }() - - to, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE, fileMode) - if err != nil { - return errorutils.CheckError(err) - } - defer func() { - e := to.Close() - if err == nil { - err = e - } - }() - - if _, err = io.Copy(to, from); err != nil { - return errorutils.CheckError(err) - } - - return errorutils.CheckError(os.Chmod(dst, fileMode)) -} - func DoubleWinPathSeparator(filePath string) string { return strings.ReplaceAll(filePath, "\\", "\\\\") } diff --git a/utils/plugins/utils_test.go b/utils/plugins/utils_test.go index 1eb94bac6..30579d244 100644 --- a/utils/plugins/utils_test.go +++ b/utils/plugins/utils_test.go @@ -2,6 +2,7 @@ package plugins import ( "fmt" + biutils "github.com/jfrog/build-info-go/utils" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" testsutils "github.com/jfrog/jfrog-client-go/utils/tests" @@ -88,7 +89,7 @@ func setupPluginsTestingEnv(t *testing.T, pluginsDirName string) string { assert.NoError(t, err) wd, err := os.Getwd() assert.NoError(t, err) - err = fileutils.CopyDir(filepath.Join(wd, "testdata", coreutils.JfrogPluginsDirName, pluginsDirName), filepath.Join(testHomeDir, coreutils.JfrogPluginsDirName), true, nil) + err = biutils.CopyDir(filepath.Join(wd, "testdata", coreutils.JfrogPluginsDirName, pluginsDirName), filepath.Join(testHomeDir, coreutils.JfrogPluginsDirName), true, nil) assert.NoError(t, err) err = coreutils.ChmodPluginsDirectoryContent() assert.NoError(t, err) diff --git a/utils/python/utils.go b/utils/python/utils.go index d5057b894..c44c0385a 100644 --- a/utils/python/utils.go +++ b/utils/python/utils.go @@ -51,15 +51,15 @@ func GetPypiRemoteRegistryFlag(tool pythonutils.PythonTool) string { return pipenvRemoteRegistryFlag } -func GetPypiRepoUrl(serverDetails *config.ServerDetails, repository string) (*url.URL, error) { +func GetPypiRepoUrl(serverDetails *config.ServerDetails, repository string) (string, error) { rtUrl, username, password, err := GetPypiRepoUrlWithCredentials(serverDetails, repository) if err != nil { - return nil, err + return "", err } if password != "" { rtUrl.User = url.UserPassword(username, password) } - return rtUrl, err + return rtUrl.String(), err } func ConfigPoetryRepo(url, username, password, configRepoName string) error { diff --git a/utils/python/utils_test.go b/utils/python/utils_test.go index 6e188d305..f36b87f1f 100644 --- a/utils/python/utils_test.go +++ b/utils/python/utils_test.go @@ -1,7 +1,8 @@ package utils import ( - "github.com/jfrog/jfrog-cli-core/v2/xray/audit" + biutils "github.com/jfrog/build-info-go/utils" + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/sca" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" "github.com/stretchr/testify/assert" "path/filepath" @@ -27,7 +28,7 @@ func initPoetryTest(t *testing.T) (string, func()) { // Create and change directory to test workspace testAbs, err := filepath.Abs(filepath.Join("..", "..", "xray", "commands", "testdata", "poetry-project")) assert.NoError(t, err) - poetryProjectPath, cleanUp := audit.CreateTestWorkspace(t, "poetry-project") - assert.NoError(t, fileutils.CopyDir(testAbs, poetryProjectPath, true, nil)) + poetryProjectPath, cleanUp := sca.CreateTestWorkspace(t, "poetry-project") + assert.NoError(t, biutils.CopyDir(testAbs, poetryProjectPath, true, nil)) return poetryProjectPath, cleanUp } diff --git a/utils/tests/utils.go b/utils/tests/utils.go index 0b8acfe3f..3211255bb 100644 --- a/utils/tests/utils.go +++ b/utils/tests/utils.go @@ -125,15 +125,15 @@ func compare(expected, actual []string) error { } // CompareTree returns true iff the two trees contain the same nodes (regardless of their order) -func CompareTree(a, b *xrayUtils.GraphNode) bool { - if a.Id != b.Id { +func CompareTree(expected, actual *xrayUtils.GraphNode) bool { + if expected.Id != actual.Id { return false } // Make sure all children are equal, when order doesn't matter - for _, nodeA := range a.Nodes { + for _, expectedNode := range expected.Nodes { found := false - for _, nodeB := range b.Nodes { - if CompareTree(nodeA, nodeB) { + for _, actualNode := range actual.Nodes { + if CompareTree(expectedNode, actualNode) { found = true break } diff --git a/utils/usage/usage.go b/utils/usage/usage.go index df1a20292..ff9660bb2 100644 --- a/utils/usage/usage.go +++ b/utils/usage/usage.go @@ -2,13 +2,13 @@ package usage import ( "fmt" + xrayutils "github.com/jfrog/jfrog-cli-core/v2/xray/utils" "golang.org/x/sync/errgroup" "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - xray "github.com/jfrog/jfrog-cli-core/v2/xray/commands/utils" "github.com/jfrog/jfrog-client-go/artifactory/usage" clientutils "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/errorutils" @@ -127,7 +127,7 @@ func (ur *UsageReporter) reportToXray(features ...ReportFeature) (err error) { err = errorutils.CheckErrorf("Xray Url is not set.") return } - serviceManager, err := xray.CreateXrayServiceManager(ur.serverDetails) + serviceManager, err := xrayutils.CreateXrayServiceManager(ur.serverDetails) if err != nil { return } diff --git a/xray/audit/jas/applicabilitymanager_test.go b/xray/audit/jas/applicabilitymanager_test.go deleted file mode 100644 index 3c8ffc5a8..000000000 --- a/xray/audit/jas/applicabilitymanager_test.go +++ /dev/null @@ -1,392 +0,0 @@ -package jas - -import ( - rtutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" - "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/jfrog/jfrog-cli-core/v2/xray/utils" - "github.com/jfrog/jfrog-client-go/xray/services" - "github.com/stretchr/testify/assert" - "os" - "path/filepath" - "testing" -) - -func TestNewApplicabilityScanManager_InputIsValid(t *testing.T) { - // Act - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() - applicabilityManager := newApplicabilityScanManager(fakeBasicXrayResults, mockDirectDependencies, scanner) - - // Assert - assert.NotEmpty(t, applicabilityManager) - assert.NotEmpty(t, applicabilityManager.scanner.configFileName) - assert.NotEmpty(t, applicabilityManager.scanner.resultsFileName) - assert.Equal(t, applicabilityManager.directDependenciesCves.Size(), 5) -} - -func TestNewApplicabilityScanManager_DependencyTreeDoesntExist(t *testing.T) { - // Act - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() - applicabilityManager := newApplicabilityScanManager(fakeBasicXrayResults, nil, scanner) - - // Assert - assert.NotEmpty(t, applicabilityManager) - assert.NotNil(t, applicabilityManager.scanner.scannerDirCleanupFunc) - assert.Len(t, applicabilityManager.scanner.workingDirs, 1) - assert.NotEmpty(t, applicabilityManager.scanner.configFileName) - assert.NotEmpty(t, applicabilityManager.scanner.resultsFileName) - assert.Equal(t, applicabilityManager.directDependenciesCves.Size(), 0) -} - -func TestNewApplicabilityScanManager_NoDirectDependenciesInScan(t *testing.T) { - // Arrange - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - var noDirectDependenciesResults = []services.ScanResponse{ - { - ScanId: "scanId_1", - Vulnerabilities: []services.Vulnerability{ - {IssueId: "issueId_1", Technology: coreutils.Pipenv.ToString(), - Cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}, {Id: "testCve3"}}, - Components: map[string]services.Component{ - "issueId_1_non_direct_dependency": {}}}, - }, - Violations: []services.Violation{ - {IssueId: "issueId_2", Technology: coreutils.Pipenv.ToString(), - Cves: []services.Cve{{Id: "testCve4"}, {Id: "testCve5"}}, - Components: map[string]services.Component{ - "issueId_2_non_direct_dependency": {}}}, - }, - }, - } - fakeBasicXrayResults[0].Vulnerabilities[0].Components["issueId_1_non_direct_dependency"] = services.Component{} - fakeBasicXrayResults[0].Violations[0].Components["issueId_2_non_direct_dependency"] = services.Component{} - - // Act - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() - applicabilityManager := newApplicabilityScanManager(noDirectDependenciesResults, mockDirectDependencies, scanner) - - // Assert - assert.NotEmpty(t, applicabilityManager) - assert.NotEmpty(t, applicabilityManager.scanner.configFileName) - assert.NotEmpty(t, applicabilityManager.scanner.resultsFileName) - // Non-direct dependencies should not be added - assert.Equal(t, 0, applicabilityManager.directDependenciesCves.Size()) -} - -func TestNewApplicabilityScanManager_MultipleDependencyTrees(t *testing.T) { - // Arrange - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - - // Act - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() - applicabilityManager := newApplicabilityScanManager(fakeBasicXrayResults, mockMultiRootDirectDependencies, scanner) - - // Assert - assert.NotEmpty(t, applicabilityManager) - assert.NotEmpty(t, applicabilityManager.scanner.configFileName) - assert.NotEmpty(t, applicabilityManager.scanner.resultsFileName) - assert.Equal(t, 5, applicabilityManager.directDependenciesCves.Size()) -} - -func TestNewApplicabilityScanManager_ViolationsDontExistInResults(t *testing.T) { - // Arrange - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - noViolationScanResponse := []services.ScanResponse{ - { - ScanId: "scanId_1", - Vulnerabilities: []services.Vulnerability{ - {IssueId: "issueId_1", Technology: coreutils.Pipenv.ToString(), - Cves: []services.Cve{{Id: "test_cve_1"}, {Id: "test_cve_2"}, {Id: "test_cve_3"}}, - Components: map[string]services.Component{"issueId_1_direct_dependency": {}}}, - }, - }, - } - - // Act - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() - applicabilityManager := newApplicabilityScanManager(noViolationScanResponse, mockDirectDependencies, scanner) - - // Assert - assert.NoError(t, err) - assert.NotEmpty(t, applicabilityManager) - assert.NotEmpty(t, applicabilityManager.scanner.configFileName) - assert.NotEmpty(t, applicabilityManager.scanner.resultsFileName) - assert.Equal(t, 3, applicabilityManager.directDependenciesCves.Size()) -} - -func TestNewApplicabilityScanManager_VulnerabilitiesDontExist(t *testing.T) { - // Arrange - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - noVulnerabilitiesScanResponse := []services.ScanResponse{ - { - ScanId: "scanId_1", - Violations: []services.Violation{ - {IssueId: "issueId_2", Technology: coreutils.Pipenv.ToString(), - Cves: []services.Cve{{Id: "test_cve_3"}, {Id: "test_cve_4"}}, - Components: map[string]services.Component{"issueId_2_direct_dependency": {}}}, - }, - }, - } - - // Act - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() - applicabilityManager := newApplicabilityScanManager(noVulnerabilitiesScanResponse, mockDirectDependencies, scanner) - - // Assert - assert.NotEmpty(t, applicabilityManager) - assert.NotEmpty(t, applicabilityManager.scanner.configFileName) - assert.NotEmpty(t, applicabilityManager.scanner.resultsFileName) - assert.Equal(t, 2, applicabilityManager.directDependenciesCves.Size()) -} - -func TestApplicabilityScanManager_ShouldRun_TechnologiesNotEligibleForScan(t *testing.T) { - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() - results, err := getApplicabilityScanResults(fakeBasicXrayResults, mockDirectDependencies, - []coreutils.Technology{coreutils.Nuget, coreutils.Go}, scanner) - - // Assert - assert.Nil(t, results) - assert.NoError(t, err) -} - -func TestApplicabilityScanManager_ShouldRun_ScanResultsAreEmpty(t *testing.T) { - // Arrange - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() - applicabilityManager := newApplicabilityScanManager(nil, mockDirectDependencies, scanner) - assert.NoError(t, err) - // Assert - eligible := applicabilityManager.shouldRunApplicabilityScan([]coreutils.Technology{coreutils.Npm}) - assert.False(t, eligible) -} - -func TestExtractXrayDirectViolations(t *testing.T) { - var xrayResponseForDirectViolationsTest = []services.ScanResponse{ - { - Violations: []services.Violation{ - {IssueId: "issueId_2", Technology: coreutils.Pipenv.ToString(), - Cves: []services.Cve{{Id: "testCve4"}, {Id: "testCve5"}}, - Components: map[string]services.Component{"issueId_2_direct_dependency": {}}}, - }, - }, - } - tests := []struct { - directDependencies []string - cvesCount int - }{ - {directDependencies: []string{"issueId_2_direct_dependency", "issueId_1_direct_dependency"}, - cvesCount: 2, - }, - // Vulnerability dependency, should be ignored by function - {directDependencies: []string{"issueId_1_direct_dependency"}, - cvesCount: 0, - }, - {directDependencies: []string{}, - cvesCount: 0, - }, - } - - for _, test := range tests { - cves := extractDirectDependenciesCvesFromScan(xrayResponseForDirectViolationsTest, test.directDependencies) - assert.Equal(t, test.cvesCount, cves.Size()) - } -} - -func TestExtractXrayDirectVulnerabilities(t *testing.T) { - var xrayResponseForDirectVulnerabilitiesTest = []services.ScanResponse{ - { - ScanId: "scanId_1", - Vulnerabilities: []services.Vulnerability{ - { - IssueId: "issueId_1", Technology: coreutils.Pipenv.ToString(), - Cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}, {Id: "testCve3"}}, - Components: map[string]services.Component{"issueId_1_direct_dependency": {}}, - }, - { - IssueId: "issueId_2", Technology: coreutils.Pipenv.ToString(), - Cves: []services.Cve{{Id: "testCve4"}, {Id: "testCve5"}}, - Components: map[string]services.Component{"issueId_2_direct_dependency": {}}, - }, - }, - }, - } - tests := []struct { - directDependencies []string - cvesCount int - }{ - { - directDependencies: []string{"issueId_1_direct_dependency"}, - cvesCount: 3, - }, - { - directDependencies: []string{"issueId_2_direct_dependency"}, - cvesCount: 2, - }, - {directDependencies: []string{}, - cvesCount: 0, - }, - } - - for _, test := range tests { - assert.Equal(t, test.cvesCount, extractDirectDependenciesCvesFromScan(xrayResponseForDirectVulnerabilitiesTest, test.directDependencies).Size()) - } -} - -func TestCreateConfigFile_VerifyFileWasCreated(t *testing.T) { - // Arrange - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() - applicabilityManager := newApplicabilityScanManager(fakeBasicXrayResults, []string{"issueId_1_direct_dependency", "issueId_2_direct_dependency"}, scanner) - - currWd, err := coreutils.GetWorkingDirectory() - assert.NoError(t, err) - err = applicabilityManager.createConfigFile(currWd) - assert.NoError(t, err) - - defer func() { - err = os.Remove(applicabilityManager.scanner.configFileName) - assert.NoError(t, err) - }() - - _, fileNotExistError := os.Stat(applicabilityManager.scanner.configFileName) - assert.NoError(t, fileNotExistError) - fileContent, err := os.ReadFile(applicabilityManager.scanner.configFileName) - assert.NoError(t, err) - assert.True(t, len(fileContent) > 0) -} - -func TestParseResults_EmptyResults_AllCvesShouldGetUnknown(t *testing.T) { - // Arrange - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() - applicabilityManager := newApplicabilityScanManager(fakeBasicXrayResults, mockDirectDependencies, scanner) - applicabilityManager.scanner.resultsFileName = filepath.Join("..", "..", "commands", "testdata", "applicability-scan", "empty-results.sarif") - - // Act - results, err := applicabilityManager.getScanResults() - - // Assert - assert.NoError(t, err) - assert.Equal(t, 5, len(results)) - for _, cveResult := range results { - assert.Equal(t, utils.ApplicabilityUndeterminedStringValue, cveResult) - } -} - -func TestParseResults_ApplicableCveExist(t *testing.T) { - // Arrange - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() - applicabilityManager := newApplicabilityScanManager(fakeBasicXrayResults, mockDirectDependencies, scanner) - applicabilityManager.scanner.resultsFileName = filepath.Join("..", "..", "commands", "testdata", "applicability-scan", "applicable-cve-results.sarif") - - // Act - results, err := applicabilityManager.getScanResults() - - // Assert - assert.NoError(t, err) - assert.Equal(t, 5, len(results)) - assert.Equal(t, utils.ApplicableStringValue, results["testCve1"]) - assert.Equal(t, utils.NotApplicableStringValue, results["testCve3"]) -} - -func TestParseResults_AllCvesNotApplicable(t *testing.T) { - // Arrange - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() - applicabilityManager := newApplicabilityScanManager(fakeBasicXrayResults, mockDirectDependencies, scanner) - applicabilityManager.scanner.resultsFileName = filepath.Join("..", "..", "commands", "testdata", "applicability-scan", "no-applicable-cves-results.sarif") - - // Act - results, err := applicabilityManager.getScanResults() - - // Assert - assert.NoError(t, err) - assert.Equal(t, 5, len(results)) - for _, cveResult := range results { - assert.Equal(t, utils.NotApplicableStringValue, cveResult) - } -} - -func TestGetExtendedScanResults_AnalyzerManagerReturnsError(t *testing.T) { - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanResults := &utils.ExtendedScanResults{XrayResults: fakeBasicXrayResults, ScannedTechnologies: []coreutils.Technology{coreutils.Yarn}} - err := RunScannersAndSetResults(scanResults, mockDirectDependencies, &fakeServerDetails, nil, nil) - - // Expect error: - assert.ErrorContains(t, err, "failed to run Applicability scan") -} diff --git a/xray/audit/jas/iacscanner_test.go b/xray/audit/jas/iacscanner_test.go deleted file mode 100644 index 430b75fdb..000000000 --- a/xray/audit/jas/iacscanner_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package jas - -import ( - rtutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" - "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/stretchr/testify/assert" - "os" - "path/filepath" - "testing" -) - -func TestNewIacScanManager(t *testing.T) { - // Act - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner([]string{"currentDir"}, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() - iacScanManager := newIacScanManager(scanner) - - // Assert - assert.NoError(t, err) - assert.NotEmpty(t, iacScanManager) - assert.NotEmpty(t, iacScanManager.scanner.configFileName) - assert.NotEmpty(t, iacScanManager.scanner.resultsFileName) - assert.NotEmpty(t, iacScanManager.scanner.workingDirs) - assert.Equal(t, &fakeServerDetails, iacScanManager.scanner.serverDetails) -} - -func TestIacScan_CreateConfigFile_VerifyFileWasCreated(t *testing.T) { - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner([]string{"currentDir"}, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() - iacScanManager := newIacScanManager(scanner) - - currWd, err := coreutils.GetWorkingDirectory() - assert.NoError(t, err) - err = iacScanManager.createConfigFile(currWd) - - defer func() { - err = os.Remove(iacScanManager.scanner.configFileName) - assert.NoError(t, err) - }() - - _, fileNotExistError := os.Stat(iacScanManager.scanner.configFileName) - assert.NoError(t, fileNotExistError) - fileContent, err := os.ReadFile(iacScanManager.scanner.configFileName) - assert.NoError(t, err) - assert.True(t, len(fileContent) > 0) -} - -func TestIacParseResults_EmptyResults(t *testing.T) { - // Arrange - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() - iacScanManager := newIacScanManager(scanner) - iacScanManager.scanner.resultsFileName = filepath.Join("..", "..", "commands", "testdata", "iac-scan", "no-violations.sarif") - - // Act - iacScanManager.iacScannerResults, err = getIacOrSecretsScanResults(iacScanManager.scanner.resultsFileName, scanner.workingDirs[0], false) - - // Assert - assert.NoError(t, err) - assert.Empty(t, iacScanManager.iacScannerResults) -} - -func TestIacParseResults_ResultsContainIacViolations(t *testing.T) { - // Arrange - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() - iacScanManager := newIacScanManager(scanner) - iacScanManager.scanner.resultsFileName = filepath.Join("..", "..", "commands", "testdata", "iac-scan", "contains-iac-violations.sarif") - - // Act - iacScanManager.iacScannerResults, err = getIacOrSecretsScanResults(iacScanManager.scanner.resultsFileName, scanner.workingDirs[0], false) - - // Assert - assert.NoError(t, err) - assert.NotEmpty(t, iacScanManager.iacScannerResults) - assert.Equal(t, 4, len(iacScanManager.iacScannerResults)) -} diff --git a/xray/audit/jas/jasmanager.go b/xray/audit/jas/jasmanager.go deleted file mode 100644 index c3304ecdf..000000000 --- a/xray/audit/jas/jasmanager.go +++ /dev/null @@ -1,161 +0,0 @@ -package jas - -import ( - "errors" - "github.com/jfrog/jfrog-cli-core/v2/utils/config" - "github.com/jfrog/jfrog-cli-core/v2/xray/utils" - "github.com/jfrog/jfrog-client-go/utils/errorutils" - "github.com/jfrog/jfrog-client-go/utils/io" - "github.com/jfrog/jfrog-client-go/utils/io/fileutils" - "github.com/jfrog/jfrog-client-go/utils/log" - "github.com/owenrumney/go-sarif/v2/sarif" - "gopkg.in/yaml.v3" - "os" - "path/filepath" -) - -var ( - skippedDirs = []string{"**/*test*/**", "**/*venv*/**", "**/*node_modules*/**", "**/*target*/**"} -) - -type ScannerCmd interface { - Run(wd string) (err error) -} - -type AdvancedSecurityScanner struct { - configFileName string - resultsFileName string - analyzerManager utils.AnalyzerManager - serverDetails *config.ServerDetails - workingDirs []string - scannerDirCleanupFunc func() error -} - -func NewAdvancedSecurityScanner(workingDirs []string, serverDetails *config.ServerDetails) (scanner *AdvancedSecurityScanner, err error) { - scanner = &AdvancedSecurityScanner{} - if scanner.analyzerManager.AnalyzerManagerFullPath, err = utils.GetAnalyzerManagerExecutable(); err != nil { - return - } - var tempDir string - if tempDir, err = fileutils.CreateTempDir(); err != nil { - return - } - scanner.scannerDirCleanupFunc = func() error { - return fileutils.RemoveTempDir(tempDir) - } - scanner.serverDetails = serverDetails - scanner.configFileName = filepath.Join(tempDir, "config.yaml") - scanner.resultsFileName = filepath.Join(tempDir, "results.sarif") - scanner.workingDirs, err = utils.GetFullPathsWorkingDirs(workingDirs) - return -} - -func (a *AdvancedSecurityScanner) Run(scannerCmd ScannerCmd) (err error) { - for _, workingDir := range a.workingDirs { - func() { - defer func() { - err = errors.Join(err, deleteJasProcessFiles(a.configFileName, a.resultsFileName)) - }() - if err = scannerCmd.Run(workingDir); err != nil { - return - } - }() - } - return -} - -func RunScannersAndSetResults(scanResults *utils.ExtendedScanResults, directDependencies []string, - serverDetails *config.ServerDetails, workingDirs []string, progress io.ProgressMgr) (err error) { - if serverDetails == nil || len(serverDetails.Url) == 0 { - log.Warn("To include 'Advanced Security' scan as part of the audit output, please run the 'jf c add' command before running this command.") - return - } - scanner, err := NewAdvancedSecurityScanner(workingDirs, serverDetails) - if err != nil { - return - } - defer func() { - cleanup := scanner.scannerDirCleanupFunc - err = errors.Join(err, cleanup()) - }() - if progress != nil { - progress.SetHeadlineMsg("Running applicability scanning") - } - scanResults.ApplicabilityScanResults, err = getApplicabilityScanResults(scanResults.XrayResults, directDependencies, scanResults.ScannedTechnologies, scanner) - if err != nil { - return - } - if progress != nil { - progress.SetHeadlineMsg("Running secrets scanning") - } - scanResults.SecretsScanResults, err = getSecretsScanResults(scanner) - if err != nil { - return - } - if progress != nil { - progress.SetHeadlineMsg("Running IaC scanning") - } - scanResults.IacScanResults, err = getIacScanResults(scanner) - return -} - -func deleteJasProcessFiles(configFile string, resultFile string) error { - exist, err := fileutils.IsFileExists(configFile, false) - if err != nil { - return err - } - if exist { - if err = os.Remove(configFile); err != nil { - return errorutils.CheckError(err) - } - } - exist, err = fileutils.IsFileExists(resultFile, false) - if err != nil { - return err - } - if exist { - err = os.Remove(resultFile) - } - return errorutils.CheckError(err) -} - -func getIacOrSecretsScanResults(resultsFileName, workingDir string, isSecret bool) ([]utils.IacOrSecretResult, error) { - report, err := sarif.Open(resultsFileName) - if errorutils.CheckError(err) != nil { - return nil, err - } - var results []*sarif.Result - if len(report.Runs) > 0 { - results = report.Runs[0].Results - } - - var iacOrSecretResults []utils.IacOrSecretResult - for _, result := range results { - // Describes a request to “suppress” a result (to exclude it from result lists) - if len(result.Suppressions) > 0 { - continue - } - text := *result.Message.Text - if isSecret { - text = hideSecret(*result.Locations[0].PhysicalLocation.Region.Snippet.Text) - } - newResult := utils.IacOrSecretResult{ - Severity: utils.GetResultSeverity(result), - File: utils.ExtractRelativePath(utils.GetResultFileName(result), workingDir), - LineColumn: utils.GetResultLocationInFile(result), - Text: text, - Type: *result.RuleID, - } - iacOrSecretResults = append(iacOrSecretResults, newResult) - } - return iacOrSecretResults, nil -} - -func createScannersConfigFile(fileName string, fileContent interface{}) error { - yamlData, err := yaml.Marshal(&fileContent) - if errorutils.CheckError(err) != nil { - return err - } - err = os.WriteFile(fileName, yamlData, 0644) - return errorutils.CheckError(err) -} diff --git a/xray/audit/jas/jasmanager_test.go b/xray/audit/jas/jasmanager_test.go deleted file mode 100644 index 960c1e9d3..000000000 --- a/xray/audit/jas/jasmanager_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package jas - -import ( - "github.com/jfrog/jfrog-cli-core/v2/xray/utils" - "os" - "testing" - - "github.com/jfrog/jfrog-cli-core/v2/utils/config" - "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/jfrog/jfrog-client-go/utils/io/fileutils" - "github.com/jfrog/jfrog-client-go/xray/services" - "github.com/stretchr/testify/assert" -) - -var fakeBasicXrayResults = []services.ScanResponse{ - { - ScanId: "scanId_1", - Vulnerabilities: []services.Vulnerability{ - {IssueId: "issueId_1", Technology: coreutils.Pipenv.ToString(), - Cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}, {Id: "testCve3"}}, - Components: map[string]services.Component{"issueId_1_direct_dependency": {}, "issueId_3_direct_dependency": {}}}, - }, - Violations: []services.Violation{ - {IssueId: "issueId_2", Technology: coreutils.Pipenv.ToString(), - Cves: []services.Cve{{Id: "testCve4"}, {Id: "testCve5"}}, - Components: map[string]services.Component{"issueId_2_direct_dependency": {}, "issueId_4_direct_dependency": {}}}, - }, - }, -} - -var mockDirectDependencies = []string{"issueId_2_direct_dependency", "issueId_1_direct_dependency"} -var mockMultiRootDirectDependencies = []string{"issueId_2_direct_dependency", "issueId_1_direct_dependency", "issueId_3_direct_dependency", "issueId_4_direct_dependency"} - -var fakeServerDetails = config.ServerDetails{ - Url: "platformUrl", - Password: "password", - User: "user", -} - -func TestGetExtendedScanResults_AnalyzerManagerDoesntExist(t *testing.T) { - tmpDir, err := fileutils.CreateTempDir() - defer func() { - assert.NoError(t, fileutils.RemoveTempDir(tmpDir)) - }() - assert.NoError(t, err) - assert.NoError(t, os.Setenv(coreutils.HomeDir, tmpDir)) - defer func() { - assert.NoError(t, os.Unsetenv(coreutils.HomeDir)) - }() - scanResults := &utils.ExtendedScanResults{XrayResults: fakeBasicXrayResults, ScannedTechnologies: []coreutils.Technology{coreutils.Yarn}} - err = RunScannersAndSetResults(scanResults, []string{"issueId_1_direct_dependency", "issueId_2_direct_dependency"}, &fakeServerDetails, nil, nil) - // Expect error: - assert.Error(t, err) -} - -func TestGetExtendedScanResults_ServerNotValid(t *testing.T) { - scanResults := &utils.ExtendedScanResults{XrayResults: fakeBasicXrayResults, ScannedTechnologies: []coreutils.Technology{coreutils.Pip}} - err := RunScannersAndSetResults(scanResults, []string{"issueId_1_direct_dependency", "issueId_2_direct_dependency"}, nil, nil, nil) - assert.NoError(t, err) -} diff --git a/xray/audit/jas/secretsscanner_test.go b/xray/audit/jas/secretsscanner_test.go deleted file mode 100644 index 9ec80fa61..000000000 --- a/xray/audit/jas/secretsscanner_test.go +++ /dev/null @@ -1,155 +0,0 @@ -package jas - -import ( - rtutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" - "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/stretchr/testify/assert" - "os" - "path/filepath" - "testing" -) - -func TestNewSecretsScanManager(t *testing.T) { - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() - secretScanManager := newSecretsScanManager(scanner) - - assert.NoError(t, err) - assert.NotEmpty(t, secretScanManager) - assert.NotEmpty(t, secretScanManager.scanner.configFileName) - assert.NotEmpty(t, secretScanManager.scanner.resultsFileName) - assert.Equal(t, &fakeServerDetails, secretScanManager.scanner.serverDetails) -} - -func TestSecretsScan_CreateConfigFile_VerifyFileWasCreated(t *testing.T) { - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() - secretScanManager := newSecretsScanManager(scanner) - - currWd, err := coreutils.GetWorkingDirectory() - assert.NoError(t, err) - err = secretScanManager.createConfigFile(currWd) - assert.NoError(t, err) - - defer func() { - err = os.Remove(secretScanManager.scanner.configFileName) - assert.NoError(t, err) - }() - - _, fileNotExistError := os.Stat(secretScanManager.scanner.configFileName) - assert.NoError(t, fileNotExistError) - fileContent, err := os.ReadFile(secretScanManager.scanner.configFileName) - assert.NoError(t, err) - assert.True(t, len(fileContent) > 0) -} - -func TestRunAnalyzerManager_ReturnsGeneralError(t *testing.T) { - defer func() { - os.Clearenv() - }() - - // Arrange - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() - secretScanManager := newSecretsScanManager(scanner) - - // Act - err = secretScanManager.runAnalyzerManager() - - // Assert - assert.Error(t, err) -} - -func TestParseResults_EmptyResults(t *testing.T) { - // Arrange - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() - secretScanManager := newSecretsScanManager(scanner) - secretScanManager.scanner.resultsFileName = filepath.Join("..", "..", "commands", "testdata", "secrets-scan", "no-secrets.sarif") - - // Act - secretScanManager.secretsScannerResults, err = getIacOrSecretsScanResults(secretScanManager.scanner.resultsFileName, scanner.workingDirs[0], false) - - // Assert - assert.NoError(t, err) - assert.Empty(t, secretScanManager.secretsScannerResults) -} - -func TestParseResults_ResultsContainSecrets(t *testing.T) { - // Arrange - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() - secretScanManager := newSecretsScanManager(scanner) - secretScanManager.scanner.resultsFileName = filepath.Join("..", "..", "commands", "testdata", "secrets-scan", "contain-secrets.sarif") - - // Act - secretScanManager.secretsScannerResults, err = getIacOrSecretsScanResults(secretScanManager.scanner.resultsFileName, scanner.workingDirs[0], false) - - // Assert - assert.NoError(t, err) - assert.NotEmpty(t, secretScanManager.secretsScannerResults) - assert.Equal(t, 7, len(secretScanManager.secretsScannerResults)) -} - -func TestGetSecretsScanResults_AnalyzerManagerReturnsError(t *testing.T) { - assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) - scanner, err := NewAdvancedSecurityScanner(nil, &fakeServerDetails) - assert.NoError(t, err) - defer func() { - if scanner.scannerDirCleanupFunc != nil { - assert.NoError(t, scanner.scannerDirCleanupFunc()) - } - }() - secretsResults, err := getSecretsScanResults(scanner) - - assert.Error(t, err) - assert.ErrorContains(t, err, "failed to run Secrets scan") - assert.Nil(t, secretsResults) -} - -func TestHideSecret(t *testing.T) { - tests := []struct { - secret string - expectedOutput string - }{ - {secret: "", expectedOutput: "***"}, - {secret: "12", expectedOutput: "***"}, - {secret: "123", expectedOutput: "***"}, - {secret: "123456789", expectedOutput: "123************"}, - {secret: "3478hfnkjhvd848446gghgfh", expectedOutput: "347************"}, - } - - for _, test := range tests { - assert.Equal(t, test.expectedOutput, hideSecret(test.secret)) - } -} diff --git a/xray/audit/java/mvn_test.go b/xray/audit/java/mvn_test.go deleted file mode 100644 index 8ff234572..000000000 --- a/xray/audit/java/mvn_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package java - -import ( - "os" - "testing" - - "github.com/jfrog/jfrog-cli-core/v2/xray/audit" - - "github.com/stretchr/testify/assert" -) - -func TestMavenTreesMultiModule(t *testing.T) { - // Create and change directory to test workspace - _, cleanUp := audit.CreateTestWorkspace(t, "maven-example") - defer cleanUp() - - // Run getModulesDependencyTrees - modulesDependencyTrees, err := buildMvnDependencyTree(&DependencyTreeParams{IgnoreConfigFile: true}) - if assert.NoError(t, err) && assert.NotEmpty(t, modulesDependencyTrees) { - // Check root module - multi := audit.GetAndAssertNode(t, modulesDependencyTrees, "org.jfrog.test:multi:3.7-SNAPSHOT") - if assert.NotNil(t, multi) { - assert.Empty(t, multi.Nodes) - // Check multi1 with a transitive dependency - multi1 := audit.GetAndAssertNode(t, modulesDependencyTrees, "org.jfrog.test:multi1:3.7-SNAPSHOT") - assert.Len(t, multi1.Nodes, 4) - commonsEmail := audit.GetAndAssertNode(t, multi1.Nodes, "org.apache.commons:commons-email:1.1") - assert.Len(t, commonsEmail.Nodes, 2) - - // Check multi2 and multi3 - multi2 := audit.GetAndAssertNode(t, modulesDependencyTrees, "org.jfrog.test:multi2:3.7-SNAPSHOT") - assert.Len(t, multi2.Nodes, 1) - multi3 := audit.GetAndAssertNode(t, modulesDependencyTrees, "org.jfrog.test:multi3:3.7-SNAPSHOT") - assert.Len(t, multi3.Nodes, 4) - } - } -} - -func TestMavenWrapperTrees(t *testing.T) { - // Create and change directory to test workspace - _, cleanUp := audit.CreateTestWorkspace(t, "maven-example-with-wrapper") - err := os.Chmod("mvnw", 0700) - defer cleanUp() - assert.NoError(t, err) - modulesDependencyTrees, err := buildMvnDependencyTree(&DependencyTreeParams{IgnoreConfigFile: true, UseWrapper: true}) - if assert.NoError(t, err) && assert.NotEmpty(t, modulesDependencyTrees) { - // Check root module - multi := audit.GetAndAssertNode(t, modulesDependencyTrees, "org.jfrog.test:multi:3.7-SNAPSHOT") - if assert.NotNil(t, multi) { - assert.Empty(t, multi.Nodes) - // Check multi1 with a transitive dependency - multi1 := audit.GetAndAssertNode(t, modulesDependencyTrees, "org.jfrog.test:multi1:3.7-SNAPSHOT") - assert.Len(t, multi1.Nodes, 7) - commonsEmail := audit.GetAndAssertNode(t, multi1.Nodes, "org.apache.commons:commons-email:1.1") - assert.Len(t, commonsEmail.Nodes, 2) - // Check multi2 and multi3 - multi2 := audit.GetAndAssertNode(t, modulesDependencyTrees, "org.jfrog.test:multi2:3.7-SNAPSHOT") - assert.Len(t, multi2.Nodes, 1) - multi3 := audit.GetAndAssertNode(t, modulesDependencyTrees, "org.jfrog.test:multi3:3.7-SNAPSHOT") - assert.Len(t, multi3.Nodes, 4) - } - } -} diff --git a/xray/audit/nuget/nuget.go b/xray/audit/nuget/nuget.go deleted file mode 100644 index a24024a94..000000000 --- a/xray/audit/nuget/nuget.go +++ /dev/null @@ -1,49 +0,0 @@ -package nuget - -import ( - "github.com/jfrog/jfrog-client-go/utils/log" - xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" - "os" - - "github.com/jfrog/build-info-go/build/utils/dotnet/solution" - "github.com/jfrog/build-info-go/entities" - "github.com/jfrog/jfrog-cli-core/v2/xray/audit" -) - -const ( - nugetPackageTypeIdentifier = "nuget://" -) - -func BuildDependencyTree() (dependencyTree []*xrayUtils.GraphNode, err error) { - wd, err := os.Getwd() - if err != nil { - return - } - sol, err := solution.Load(wd, "", log.Logger) - if err != nil { - return - } - buildInfo, err := sol.BuildInfo("", log.Logger) - if err != nil { - return - } - dependencyTree = parseNugetDependencyTree(buildInfo) - return -} - -func parseNugetDependencyTree(buildInfo *entities.BuildInfo) (nodes []*xrayUtils.GraphNode) { - for _, module := range buildInfo.Modules { - treeMap := make(map[string][]string) - for _, dependency := range module.Dependencies { - dependencyId := nugetPackageTypeIdentifier + dependency.Id - parent := nugetPackageTypeIdentifier + dependency.RequestedBy[0][0] - if children, ok := treeMap[parent]; ok { - treeMap[parent] = append(children, dependencyId) - } else { - treeMap[parent] = []string{dependencyId} - } - } - nodes = append(nodes, audit.BuildXrayDependencyTree(treeMap, nugetPackageTypeIdentifier+module.Id)) - } - return -} diff --git a/xray/audit/python/python_test.go b/xray/audit/python/python_test.go deleted file mode 100644 index 5817e05d6..000000000 --- a/xray/audit/python/python_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package python - -import ( - "path/filepath" - "testing" - - "github.com/jfrog/build-info-go/utils/pythonutils" - "github.com/jfrog/jfrog-cli-core/v2/xray/audit" - "github.com/stretchr/testify/assert" -) - -func TestBuildPipDependencyListSetuppy(t *testing.T) { - // Create and change directory to test workspace - _, cleanUp := audit.CreateTestWorkspace(t, filepath.Join("pip-project", "setuppyproject")) - defer cleanUp() - // Run getModulesDependencyTrees - rootNode, err := BuildDependencyTree(&AuditPython{Tool: pythonutils.Pip}) - assert.NoError(t, err) - assert.Len(t, rootNode, 1) - if len(rootNode) > 0 { - assert.NotEmpty(t, rootNode[0].Nodes) - if rootNode[0].Nodes != nil { - // Test direct dependency - directDepNode := audit.GetAndAssertNode(t, rootNode[0].Nodes, "pip-example:1.2.3") - // Test child module - childNode := audit.GetAndAssertNode(t, directDepNode.Nodes, "pexpect:4.8.0") - // Test sub child module - audit.GetAndAssertNode(t, childNode.Nodes, "ptyprocess:0.7.0") - } - } -} - -func TestPipDependencyListRequirementsFallback(t *testing.T) { - // Create and change directory to test workspace - _, cleanUp := audit.CreateTestWorkspace(t, filepath.Join("pip-project", "requirementsproject")) - defer cleanUp() - // No requirements file field specified, expect the command to use the fallback 'pip install -r requirements.txt' command - rootNode, err := BuildDependencyTree(&AuditPython{Tool: pythonutils.Pip}) - assert.NoError(t, err) - assert.Len(t, rootNode, 1) - if assert.True(t, len(rootNode[0].Nodes) > 2) { - childNode := audit.GetAndAssertNode(t, rootNode[0].Nodes, "pexpect:4.7.0") - if childNode != nil { - // Test child module - audit.GetAndAssertNode(t, childNode.Nodes, "ptyprocess:0.7.0") - } - } -} - -func TestBuildPipDependencyListRequirements(t *testing.T) { - // Create and change directory to test workspace - _, cleanUp := audit.CreateTestWorkspace(t, filepath.Join("pip-project", "requirementsproject")) - defer cleanUp() - // Run getModulesDependencyTrees - rootNode, err := BuildDependencyTree(&AuditPython{Tool: pythonutils.Pip, PipRequirementsFile: "requirements.txt"}) - assert.NoError(t, err) - assert.Len(t, rootNode, 1) - if len(rootNode) > 0 { - assert.NotEmpty(t, rootNode[0].Nodes) - if rootNode[0].Nodes != nil { - // Test root module - directDepNode := audit.GetAndAssertNode(t, rootNode[0].Nodes, "pexpect:4.7.0") - // Test child module - audit.GetAndAssertNode(t, directDepNode.Nodes, "ptyprocess:0.7.0") - } - } -} - -func TestBuildPipenvDependencyList(t *testing.T) { - // Create and change directory to test workspace - _, cleanUp := audit.CreateTestWorkspace(t, "pipenv-project") - defer cleanUp() - // Run getModulesDependencyTrees - rootNode, err := BuildDependencyTree(&AuditPython{Tool: pythonutils.Pipenv}) - if err != nil { - t.Fatal(err) - } - assert.Len(t, rootNode, 1) - if len(rootNode) > 0 { - assert.NotEmpty(t, rootNode[0].Nodes) - // Test child module - childNode := audit.GetAndAssertNode(t, rootNode[0].Nodes, "pexpect:4.8.0") - // Test sub child module - if assert.NotNil(t, childNode) { - audit.GetAndAssertNode(t, childNode.Nodes, "ptyprocess:0.7.0") - } - } -} - -func TestBuildPoetryDependencyList(t *testing.T) { - // Create and change directory to test workspace - _, cleanUp := audit.CreateTestWorkspace(t, "poetry-project") - defer cleanUp() - // Run getModulesDependencyTrees - rootNode, err := BuildDependencyTree(&AuditPython{Tool: pythonutils.Poetry}) - if err != nil { - t.Fatal(err) - } - assert.Len(t, rootNode, 1) - if len(rootNode) > 0 { - assert.NotEmpty(t, rootNode[0].Nodes) - // Test child module - childNode := audit.GetAndAssertNode(t, rootNode[0].Nodes, "pytest:5.4.3") - // Test sub child module - if assert.NotNil(t, childNode) { - audit.GetAndAssertNode(t, childNode.Nodes, "packaging:22.0") - } - } -} - -func TestGetPipInstallArgs(t *testing.T) { - assert.Equal(t, []string{"-m", "pip", "install", "."}, getPipInstallArgs("")) - assert.Equal(t, []string{"-m", "pip", "install", "-r", "requirements.txt"}, getPipInstallArgs("requirements.txt")) -} diff --git a/xray/commands/audit/audit.go b/xray/commands/audit/audit.go new file mode 100644 index 000000000..285703371 --- /dev/null +++ b/xray/commands/audit/audit.go @@ -0,0 +1,201 @@ +package audit + +import ( + "errors" + rtutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-cli-core/v2/xray/scangraph" + clientutils "github.com/jfrog/jfrog-client-go/utils" + "github.com/jfrog/jfrog-client-go/utils/log" + "github.com/jfrog/jfrog-client-go/xray" + "github.com/jfrog/jfrog-client-go/xray/services" + "golang.org/x/sync/errgroup" + "os" + + xrayutils "github.com/jfrog/jfrog-cli-core/v2/xray/utils" +) + +type AuditCommand struct { + watches []string + projectKey string + targetRepoPath string + IncludeVulnerabilities bool + IncludeLicenses bool + Fail bool + PrintExtendedTable bool + AuditParams +} + +func NewGenericAuditCommand() *AuditCommand { + return &AuditCommand{AuditParams: *NewAuditParams()} +} + +func (auditCmd *AuditCommand) SetWatches(watches []string) *AuditCommand { + auditCmd.watches = watches + return auditCmd +} + +func (auditCmd *AuditCommand) SetProject(project string) *AuditCommand { + auditCmd.projectKey = project + return auditCmd +} + +func (auditCmd *AuditCommand) SetTargetRepoPath(repoPath string) *AuditCommand { + auditCmd.targetRepoPath = repoPath + return auditCmd +} + +func (auditCmd *AuditCommand) SetIncludeVulnerabilities(include bool) *AuditCommand { + auditCmd.IncludeVulnerabilities = include + return auditCmd +} + +func (auditCmd *AuditCommand) SetIncludeLicenses(include bool) *AuditCommand { + auditCmd.IncludeLicenses = include + return auditCmd +} + +func (auditCmd *AuditCommand) SetFail(fail bool) *AuditCommand { + auditCmd.Fail = fail + return auditCmd +} + +func (auditCmd *AuditCommand) SetPrintExtendedTable(printExtendedTable bool) *AuditCommand { + auditCmd.PrintExtendedTable = printExtendedTable + return auditCmd +} + +func (auditCmd *AuditCommand) CreateXrayGraphScanParams() *services.XrayGraphScanParams { + params := &services.XrayGraphScanParams{ + RepoPath: auditCmd.targetRepoPath, + Watches: auditCmd.watches, + ScanType: services.Dependency, + } + if auditCmd.projectKey == "" { + params.ProjectKey = os.Getenv(coreutils.Project) + } else { + params.ProjectKey = auditCmd.projectKey + } + params.IncludeVulnerabilities = auditCmd.IncludeVulnerabilities + params.IncludeLicenses = auditCmd.IncludeLicenses + return params +} + +func (auditCmd *AuditCommand) Run() (err error) { + workingDirs, err := coreutils.GetFullPathsWorkingDirs(auditCmd.workingDirs) + if err != nil { + return + } + auditParams := NewAuditParams(). + SetXrayGraphScanParams(auditCmd.CreateXrayGraphScanParams()). + SetWorkingDirs(workingDirs). + SetMinSeverityFilter(auditCmd.minSeverityFilter). + SetFixableOnly(auditCmd.fixableOnly). + SetGraphBasicParams(auditCmd.AuditBasicParams) + auditResults, err := RunAudit(auditParams) + if err != nil { + return + } + if auditCmd.Progress() != nil { + if err = auditCmd.Progress().Quit(); err != nil { + return + } + } + var messages []string + if !auditResults.ExtendedScanResults.EntitledForJas { + messages = []string{coreutils.PrintTitle("The ‘jf audit’ command also supports JFrog Advanced Security features, such as 'Contextual Analysis', 'Secret Detection', 'IaC Scan' and ‘SAST’.\nThis feature isn't enabled on your system. Read more - ") + coreutils.PrintLink("https://jfrog.com/xray/")} + } + // Print Scan results on all cases except if errors accrued on SCA scan and no security/license issues found. + printScanResults := !(auditResults.ScaError != nil && xrayutils.IsEmptyScanResponse(auditResults.ExtendedScanResults.XrayResults)) + if printScanResults { + err = xrayutils.PrintScanResults(auditResults.ExtendedScanResults, + nil, + auditCmd.OutputFormat(), + auditCmd.IncludeVulnerabilities, + auditCmd.IncludeLicenses, + auditResults.IsMultipleRootProject, + auditCmd.PrintExtendedTable, false, messages, + ) + if err != nil { + return + } + } + if err = errors.Join(auditResults.ScaError, auditResults.JasError); err != nil { + return + } + + // Only in case Xray's context was given (!auditCmd.IncludeVulnerabilities), and the user asked to fail the build accordingly, do so. + if auditCmd.Fail && !auditCmd.IncludeVulnerabilities && xrayutils.CheckIfFailBuild(auditResults.ExtendedScanResults.XrayResults) { + err = xrayutils.NewFailBuildError() + } + return +} + +func (auditCmd *AuditCommand) CommandName() string { + return "generic_audit" +} + +type Results struct { + IsMultipleRootProject bool + ScaError error + JasError error + ExtendedScanResults *xrayutils.ExtendedScanResults +} + +func NewAuditResults() *Results { + return &Results{ExtendedScanResults: &xrayutils.ExtendedScanResults{}} +} + +// Runs an audit scan based on the provided auditParams. +// Returns an audit Results object containing all the scan results. +// If the current server is entitled for JAS, the advanced security results will be included in the scan results. +func RunAudit(auditParams *AuditParams) (results *Results, err error) { + // Initialize Results struct + results = NewAuditResults() + + serverDetails, err := auditParams.ServerDetails() + if err != nil { + return + } + var xrayManager *xray.XrayServicesManager + if xrayManager, auditParams.xrayVersion, err = xrayutils.CreateXrayServiceManagerAndGetVersion(serverDetails); err != nil { + return + } + if err = clientutils.ValidateMinimumVersion(clientutils.Xray, auditParams.xrayVersion, scangraph.GraphScanMinXrayVersion); err != nil { + return + } + results.ExtendedScanResults.XrayVersion = auditParams.xrayVersion + results.ExtendedScanResults.EntitledForJas, err = isEntitledForJas(xrayManager, auditParams.xrayVersion) + if err != nil { + return + } + + errGroup := new(errgroup.Group) + if results.ExtendedScanResults.EntitledForJas { + // Download (if needed) the analyzer manager in a background routine. + errGroup.Go(rtutils.DownloadAnalyzerManagerIfNeeded) + } + + // The sca scan doesn't require the analyzer manager, so it can run separately from the analyzer manager download routine. + results.ScaError = runScaScan(auditParams, results) + + // Wait for the Download of the AnalyzerManager to complete. + if err = errGroup.Wait(); err != nil { + return + } + + // Run scanners only if the user is entitled for Advanced Security + if results.ExtendedScanResults.EntitledForJas { + results.JasError = runJasScannersAndSetResults(results.ExtendedScanResults, auditParams.DirectDependencies(), serverDetails, auditParams.workingDirs, auditParams.Progress(), auditParams.xrayGraphScanParams.MultiScanId) + } + return +} + +func isEntitledForJas(xrayManager *xray.XrayServicesManager, xrayVersion string) (entitled bool, err error) { + if e := clientutils.ValidateMinimumVersion(clientutils.Xray, xrayVersion, xrayutils.EntitlementsMinVersion); e != nil { + log.Debug(e) + return + } + entitled, err = xrayManager.IsEntitled(xrayutils.ApplicabilityFeatureId) + return +} diff --git a/xray/commands/audit/auditparams.go b/xray/commands/audit/auditparams.go new file mode 100644 index 000000000..69c83a55f --- /dev/null +++ b/xray/commands/audit/auditparams.go @@ -0,0 +1,77 @@ +package audit + +import ( + xrayutils "github.com/jfrog/jfrog-cli-core/v2/xray/utils" + "github.com/jfrog/jfrog-client-go/xray/services" +) + +type AuditParams struct { + xrayGraphScanParams *services.XrayGraphScanParams + workingDirs []string + installFunc func(tech string) error + fixableOnly bool + minSeverityFilter string + *xrayutils.AuditBasicParams + xrayVersion string +} + +func NewAuditParams() *AuditParams { + return &AuditParams{ + xrayGraphScanParams: &services.XrayGraphScanParams{}, + AuditBasicParams: &xrayutils.AuditBasicParams{}, + } +} + +func (params *AuditParams) InstallFunc() func(tech string) error { + return params.installFunc +} + +func (params *AuditParams) XrayGraphScanParams() *services.XrayGraphScanParams { + return params.xrayGraphScanParams +} + +func (params *AuditParams) WorkingDirs() []string { + return params.workingDirs +} + +func (params *AuditParams) XrayVersion() string { + return params.xrayVersion +} + +func (params *AuditParams) SetXrayGraphScanParams(xrayGraphScanParams *services.XrayGraphScanParams) *AuditParams { + params.xrayGraphScanParams = xrayGraphScanParams + return params +} + +func (params *AuditParams) SetGraphBasicParams(gbp *xrayutils.AuditBasicParams) *AuditParams { + params.AuditBasicParams = gbp + return params +} + +func (params *AuditParams) SetWorkingDirs(workingDirs []string) *AuditParams { + params.workingDirs = workingDirs + return params +} + +func (params *AuditParams) SetInstallFunc(installFunc func(tech string) error) *AuditParams { + params.installFunc = installFunc + return params +} + +func (params *AuditParams) FixableOnly() bool { + return params.fixableOnly +} + +func (params *AuditParams) SetFixableOnly(fixable bool) *AuditParams { + params.fixableOnly = fixable + return params +} + +func (params *AuditParams) MinSeverityFilter() string { + return params.minSeverityFilter +} + +func (params *AuditParams) SetMinSeverityFilter(minSeverityFilter string) *AuditParams { + params.minSeverityFilter = minSeverityFilter + return params +} diff --git a/xray/commands/audit/generic/auditmanager.go b/xray/commands/audit/generic/auditmanager.go deleted file mode 100644 index 8b535f7a3..000000000 --- a/xray/commands/audit/generic/auditmanager.go +++ /dev/null @@ -1,314 +0,0 @@ -package audit - -import ( - "errors" - "fmt" - "github.com/jfrog/build-info-go/utils/pythonutils" - "github.com/jfrog/gofrog/datastructures" - rtutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" - "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/jfrog/jfrog-cli-core/v2/xray/audit" - _go "github.com/jfrog/jfrog-cli-core/v2/xray/audit/go" - "github.com/jfrog/jfrog-cli-core/v2/xray/audit/jas" - "github.com/jfrog/jfrog-cli-core/v2/xray/audit/java" - "github.com/jfrog/jfrog-cli-core/v2/xray/audit/npm" - "github.com/jfrog/jfrog-cli-core/v2/xray/audit/nuget" - "github.com/jfrog/jfrog-cli-core/v2/xray/audit/python" - "github.com/jfrog/jfrog-cli-core/v2/xray/audit/yarn" - commandsutils "github.com/jfrog/jfrog-cli-core/v2/xray/commands/utils" - xrayutils "github.com/jfrog/jfrog-cli-core/v2/xray/utils" - clientutils "github.com/jfrog/jfrog-client-go/utils" - "github.com/jfrog/jfrog-client-go/utils/errorutils" - "github.com/jfrog/jfrog-client-go/utils/log" - "github.com/jfrog/jfrog-client-go/xray" - "github.com/jfrog/jfrog-client-go/xray/services" - xrayCmdUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" - "golang.org/x/sync/errgroup" - "os" -) - -type Params struct { - xrayGraphScanParams *services.XrayGraphScanParams - workingDirs []string - installFunc func(tech string) error - fixableOnly bool - minSeverityFilter string - *xrayutils.GraphBasicParams - xrayVersion string -} - -func NewAuditParams() *Params { - return &Params{ - xrayGraphScanParams: &services.XrayGraphScanParams{}, - GraphBasicParams: &xrayutils.GraphBasicParams{}, - } -} - -func (params *Params) InstallFunc() func(tech string) error { - return params.installFunc -} - -func (params *Params) XrayGraphScanParams() *services.XrayGraphScanParams { - return params.xrayGraphScanParams -} - -func (params *Params) WorkingDirs() []string { - return params.workingDirs -} - -func (params *Params) XrayVersion() string { - return params.xrayVersion -} - -func (params *Params) SetXrayGraphScanParams(xrayGraphScanParams *services.XrayGraphScanParams) *Params { - params.xrayGraphScanParams = xrayGraphScanParams - return params -} - -func (params *Params) SetGraphBasicParams(gbp *xrayutils.GraphBasicParams) *Params { - params.GraphBasicParams = gbp - return params -} - -func (params *Params) SetWorkingDirs(workingDirs []string) *Params { - params.workingDirs = workingDirs - return params -} - -func (params *Params) SetInstallFunc(installFunc func(tech string) error) *Params { - params.installFunc = installFunc - return params -} - -func (params *Params) FixableOnly() bool { - return params.fixableOnly -} - -func (params *Params) SetFixableOnly(fixable bool) *Params { - params.fixableOnly = fixable - return params -} - -func (params *Params) MinSeverityFilter() string { - return params.minSeverityFilter -} - -func (params *Params) SetMinSeverityFilter(minSeverityFilter string) *Params { - params.minSeverityFilter = minSeverityFilter - return params -} - -type Results struct { - IsMultipleRootProject bool - ScaError error - JasError error - ExtendedScanResults *xrayutils.ExtendedScanResults -} - -func NewAuditResults() *Results { - return &Results{ExtendedScanResults: &xrayutils.ExtendedScanResults{}} -} - -// Runs an audit scan based on the provided auditParams. -// Returns an audit Results object containing all the scan results. -// If the current server is entitled for JAS, the advanced security results will be included in the scan results. -func RunAudit(auditParams *Params) (results *Results, err error) { - // Initialize Results struct - results = NewAuditResults() - - serverDetails, err := auditParams.ServerDetails() - if err != nil { - return - } - var xrayManager *xray.XrayServicesManager - xrayManager, auditParams.xrayVersion, err = commandsutils.CreateXrayServiceManagerAndGetVersion(serverDetails) - if err != nil { - return - } - if err = clientutils.ValidateMinimumVersion(clientutils.Xray, auditParams.xrayVersion, commandsutils.GraphScanMinXrayVersion); err != nil { - return - } - results.ExtendedScanResults.EntitledForJas, err = isEntitledForJas(xrayManager, auditParams.xrayVersion) - if err != nil { - return - } - - errGroup := new(errgroup.Group) - if results.ExtendedScanResults.EntitledForJas { - // Download (if needed) the analyzer manager in a background routine. - errGroup.Go(rtutils.DownloadAnalyzerManagerIfNeeded) - } - - // The sca scan doesn't require the analyzer manager, so it can run separately from the analyzer manager download routine. - results.ScaError = runScaScan(auditParams, results) - - // Wait for the Download of the AnalyzerManager to complete. - if err = errGroup.Wait(); err != nil { - return - } - - // Run scanners only if the user is entitled for Advanced Security - if results.ExtendedScanResults.EntitledForJas { - results.JasError = jas.RunScannersAndSetResults(results.ExtendedScanResults, auditParams.DirectDependencies(), serverDetails, auditParams.workingDirs, auditParams.Progress()) - } - return -} - -func isEntitledForJas(xrayManager *xray.XrayServicesManager, xrayVersion string) (entitled bool, err error) { - if e := clientutils.ValidateMinimumVersion(clientutils.Xray, xrayVersion, xrayutils.EntitlementsMinVersion); e != nil { - log.Debug(e) - return - } - entitled, err = xrayManager.IsEntitled(xrayutils.ApplicabilityFeatureId) - return -} - -func runScaScan(params *Params, results *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 *Params, results *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 = commandsutils.DetectedTechnologies() - } - 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.GraphBasicParams, tech) - if techErr != nil { - err = errors.Join(err, fmt.Errorf("failed while building '%s' dependency tree:\n%s\n", tech, techErr.Error())) - continue - } - if len(flattenTree) == 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 := commandsutils.NewScanGraphParams(). - SetServerDetails(serverDetails). - SetXrayGraphScanParams(params.xrayGraphScanParams). - SetXrayVersion(params.xrayVersion). - SetFixableOnly(params.fixableOnly). - SetSeverityLevel(params.minSeverityFilter) - techResults, techErr := audit.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 = audit.BuildImpactPathsForScanResponse(techResults, fullDependencyTrees) - if tech == coreutils.Pip { - params.AppendDirectDependencies(getDirectDependenciesFromTree(flattenTree)) - - } else { - params.AppendDirectDependencies(getDirectDependenciesFromTree(fullDependencyTrees)) - } - results.ExtendedScanResults.XrayResults = append(results.ExtendedScanResults.XrayResults, techResults...) - if !results.IsMultipleRootProject { - results.IsMultipleRootProject = len(flattenTree) > 1 - } - results.ExtendedScanResults.ScannedTechnologies = append(results.ExtendedScanResults.ScannedTechnologies, tech) - } - return -} - -// This function retrieves the dependency trees of the scanned project and extracts a set that contains only the direct dependencies. -func getDirectDependenciesFromTree(dependencyTrees []*xrayCmdUtils.GraphNode) []string { - directDependencies := datastructures.MakeSet[string]() - for _, tree := range dependencyTrees { - for _, node := range tree.Nodes { - directDependencies.Add(node.Id) - } - } - return directDependencies.ToSlice() -} - -func GetTechDependencyTree(params *xrayutils.GraphBasicParams, tech coreutils.Technology) (flatTree []*xrayCmdUtils.GraphNode, fullDependencyTrees []*xrayCmdUtils.GraphNode, err error) { - if params.Progress() != nil { - params.Progress().SetHeadlineMsg(fmt.Sprintf("Calculating %v dependencies", tech.ToFormal())) - } - serverDetails, err := params.ServerDetails() - if err != nil { - return - } - switch tech { - case coreutils.Maven, coreutils.Gradle: - fullDependencyTrees, err = getJavaDependencyTree(params, tech) - case coreutils.Npm: - fullDependencyTrees, err = npm.BuildDependencyTree(params.Args()) - case coreutils.Yarn: - fullDependencyTrees, err = yarn.BuildDependencyTree() - case coreutils.Go: - fullDependencyTrees, err = _go.BuildDependencyTree(serverDetails, params.DepsRepo()) - case coreutils.Pipenv, coreutils.Pip, coreutils.Poetry: - fullDependencyTrees, err = python.BuildDependencyTree(&python.AuditPython{ - Server: serverDetails, - Tool: pythonutils.PythonTool(tech), - RemotePypiRepo: params.DepsRepo(), - PipRequirementsFile: params.PipRequirementsFile()}) - case coreutils.Nuget: - fullDependencyTrees, err = nuget.BuildDependencyTree() - default: - err = errorutils.CheckErrorf("%s is currently not supported", string(tech)) - } - if err != nil { - return nil, nil, err - } - // Flatten the graph to speed up the ScanGraph request - flatTree, err = services.FlattenGraph(fullDependencyTrees) - return -} - -func getJavaDependencyTree(params *xrayutils.GraphBasicParams, tech coreutils.Technology) ([]*xrayCmdUtils.GraphNode, error) { - serverDetails, err := params.ServerDetails() - if err != nil { - return nil, err - } - return java.BuildDependencyTree(&java.DependencyTreeParams{ - Tool: tech, - InsecureTls: params.InsecureTls(), - IgnoreConfigFile: params.IgnoreConfigFile(), - ExcludeTestDeps: params.ExcludeTestDependencies(), - UseWrapper: params.UseWrapper(), - Server: serverDetails, - DepsRepo: params.DepsRepo(), - }) -} diff --git a/xray/commands/audit/generic/generic.go b/xray/commands/audit/generic/generic.go deleted file mode 100644 index 4e182632b..000000000 --- a/xray/commands/audit/generic/generic.go +++ /dev/null @@ -1,130 +0,0 @@ -package audit - -import ( - "errors" - "os" - - "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - xrutils "github.com/jfrog/jfrog-cli-core/v2/xray/utils" - "github.com/jfrog/jfrog-client-go/xray/services" -) - -type GenericAuditCommand struct { - watches []string - projectKey string - targetRepoPath string - IncludeVulnerabilities bool - IncludeLicenses bool - Fail bool - PrintExtendedTable bool - Params -} - -func NewGenericAuditCommand() *GenericAuditCommand { - return &GenericAuditCommand{Params: *NewAuditParams()} -} - -func (auditCmd *GenericAuditCommand) SetWatches(watches []string) *GenericAuditCommand { - auditCmd.watches = watches - return auditCmd -} - -func (auditCmd *GenericAuditCommand) SetProject(project string) *GenericAuditCommand { - auditCmd.projectKey = project - return auditCmd -} - -func (auditCmd *GenericAuditCommand) SetTargetRepoPath(repoPath string) *GenericAuditCommand { - auditCmd.targetRepoPath = repoPath - return auditCmd -} - -func (auditCmd *GenericAuditCommand) SetIncludeVulnerabilities(include bool) *GenericAuditCommand { - auditCmd.IncludeVulnerabilities = include - return auditCmd -} - -func (auditCmd *GenericAuditCommand) SetIncludeLicenses(include bool) *GenericAuditCommand { - auditCmd.IncludeLicenses = include - return auditCmd -} - -func (auditCmd *GenericAuditCommand) SetFail(fail bool) *GenericAuditCommand { - auditCmd.Fail = fail - return auditCmd -} - -func (auditCmd *GenericAuditCommand) SetPrintExtendedTable(printExtendedTable bool) *GenericAuditCommand { - auditCmd.PrintExtendedTable = printExtendedTable - return auditCmd -} - -func (auditCmd *GenericAuditCommand) CreateXrayGraphScanParams() *services.XrayGraphScanParams { - params := &services.XrayGraphScanParams{ - RepoPath: auditCmd.targetRepoPath, - Watches: auditCmd.watches, - ScanType: services.Dependency, - } - if auditCmd.projectKey == "" { - params.ProjectKey = os.Getenv(coreutils.Project) - } else { - params.ProjectKey = auditCmd.projectKey - } - params.IncludeVulnerabilities = auditCmd.IncludeVulnerabilities - params.IncludeLicenses = auditCmd.IncludeLicenses - return params -} - -func (auditCmd *GenericAuditCommand) Run() (err error) { - workingDirs, err := xrutils.GetFullPathsWorkingDirs(auditCmd.workingDirs) - if err != nil { - return - } - auditParams := NewAuditParams(). - SetXrayGraphScanParams(auditCmd.CreateXrayGraphScanParams()). - SetWorkingDirs(workingDirs). - SetMinSeverityFilter(auditCmd.minSeverityFilter). - SetFixableOnly(auditCmd.fixableOnly). - SetGraphBasicParams(auditCmd.GraphBasicParams) - auditResults, err := RunAudit(auditParams) - if err != nil { - return - } - if auditCmd.Progress() != nil { - if err = auditCmd.Progress().Quit(); err != nil { - return - } - } - var messages []string - if !auditResults.ExtendedScanResults.EntitledForJas { - messages = []string{coreutils.PrintTitle("The ‘jf audit’ command also supports JFrog Advanced Security features, such as 'Contextual Analysis', 'Secret Detection', 'IaC Scan'.\nThis feature isn't enabled on your system. Read more - ") + coreutils.PrintLink("https://jfrog.com/xray/")} - } - // Print Scan results on all cases except if errors accrued on SCA scan and no security/license issues found. - printScanResults := !(auditResults.ScaError != nil && xrutils.IsEmptyScanResponse(auditResults.ExtendedScanResults.XrayResults)) - if printScanResults { - err = xrutils.PrintScanResults(auditResults.ExtendedScanResults, - nil, - auditCmd.OutputFormat(), - auditCmd.IncludeVulnerabilities, - auditCmd.IncludeLicenses, - auditResults.IsMultipleRootProject, - auditCmd.PrintExtendedTable, false, messages, - ) - if err != nil { - return - } - } - if err = errors.Join(auditResults.ScaError, auditResults.JasError); err != nil { - return - } - - // Only in case Xray's context was given (!auditCmd.IncludeVulnerabilities), and the user asked to fail the build accordingly, do so. - if auditCmd.Fail && !auditCmd.IncludeVulnerabilities && xrutils.CheckIfFailBuild(auditResults.ExtendedScanResults.XrayResults) { - err = xrutils.NewFailBuildError() - } - return -} - -func (auditCmd *GenericAuditCommand) CommandName() string { - return "generic_audit" -} diff --git a/xray/audit/jas/applicabilitymanager.go b/xray/commands/audit/jas/applicability/applicabilitymanager.go similarity index 56% rename from xray/audit/jas/applicabilitymanager.go rename to xray/commands/audit/jas/applicability/applicabilitymanager.go index 4c99df9cc..8c46f4865 100644 --- a/xray/audit/jas/applicabilitymanager.go +++ b/xray/commands/audit/jas/applicability/applicabilitymanager.go @@ -1,16 +1,18 @@ -package jas +package applicability import ( + "path/filepath" + + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/jas" + "github.com/jfrog/gofrog/datastructures" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-core/v2/xray/utils" - "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/log" "github.com/jfrog/jfrog-client-go/xray/services" "github.com/owenrumney/go-sarif/v2/sarif" "golang.org/x/exp/maps" "golang.org/x/exp/slices" - "strings" ) const ( @@ -18,6 +20,13 @@ const ( applicabilityScanCommand = "ca" ) +type ApplicabilityScanManager struct { + applicabilityScanResults []*sarif.Run + directDependenciesCves []string + xrayResults []services.ScanResponse + scanner *jas.JasScanner +} + // The getApplicabilityScanResults function runs the applicability scan flow, which includes the following steps: // Creating an ApplicabilityScanManager object. // Checking if the scanned project is eligible for applicability scan. @@ -27,8 +36,8 @@ const ( // map[string]string: A map containing the applicability result of each XRAY CVE. // bool: true if the user is entitled to the applicability scan, false otherwise. // error: An error object (if any). -func getApplicabilityScanResults(xrayResults []services.ScanResponse, directDependencies []string, - scannedTechnologies []coreutils.Technology, scanner *AdvancedSecurityScanner) (results map[string]string, err error) { +func RunApplicabilityScan(xrayResults []services.ScanResponse, directDependencies []string, + scannedTechnologies []coreutils.Technology, scanner *jas.JasScanner) (results []*sarif.Run, err error) { applicabilityScanManager := newApplicabilityScanManager(xrayResults, directDependencies, scanner) if !applicabilityScanManager.shouldRunApplicabilityScan(scannedTechnologies) { log.Debug("The technologies that have been scanned are currently not supported for contextual analysis scanning, or we couldn't find any vulnerable direct dependencies. Skipping....") @@ -42,17 +51,10 @@ func getApplicabilityScanResults(xrayResults []services.ScanResponse, directDepe return } -type ApplicabilityScanManager struct { - applicabilityScanResults map[string]string - directDependenciesCves *datastructures.Set[string] - xrayResults []services.ScanResponse - scanner *AdvancedSecurityScanner -} - -func newApplicabilityScanManager(xrayScanResults []services.ScanResponse, directDependencies []string, scanner *AdvancedSecurityScanner) (manager *ApplicabilityScanManager) { +func newApplicabilityScanManager(xrayScanResults []services.ScanResponse, directDependencies []string, scanner *jas.JasScanner) (manager *ApplicabilityScanManager) { directDependenciesCves := extractDirectDependenciesCvesFromScan(xrayScanResults, directDependencies) return &ApplicabilityScanManager{ - applicabilityScanResults: map[string]string{}, + applicabilityScanResults: []*sarif.Run{}, directDependenciesCves: directDependenciesCves, xrayResults: xrayScanResults, scanner: scanner, @@ -61,7 +63,7 @@ func newApplicabilityScanManager(xrayScanResults []services.ScanResponse, direct // This function gets a list of xray scan responses that contain direct and indirect vulnerabilities and returns only direct // vulnerabilities of the scanned project, ignoring indirect vulnerabilities -func extractDirectDependenciesCvesFromScan(xrayScanResults []services.ScanResponse, directDependencies []string) *datastructures.Set[string] { +func extractDirectDependenciesCvesFromScan(xrayScanResults []services.ScanResponse, directDependencies []string) []string { directsCves := datastructures.MakeSet[string]() for _, scanResult := range xrayScanResults { for _, vulnerability := range scanResult.Vulnerabilities { @@ -84,7 +86,7 @@ func extractDirectDependenciesCvesFromScan(xrayScanResults []services.ScanRespon } } - return directsCves + return directsCves.ToSlice() } func isDirectComponents(components []string, directDependencies []string) bool { @@ -96,32 +98,32 @@ func isDirectComponents(components []string, directDependencies []string) bool { return false } -func (a *ApplicabilityScanManager) Run(wd string) (err error) { - if len(a.scanner.workingDirs) > 1 { +func (asm *ApplicabilityScanManager) Run(wd string) (err error) { + if len(asm.scanner.WorkingDirs) > 1 { log.Info("Running applicability scanning in the", wd, "directory...") } else { log.Info("Running applicability scanning...") } - if err = a.createConfigFile(wd); err != nil { + if err = asm.createConfigFile(wd); err != nil { return } - if err = a.runAnalyzerManager(); err != nil { + if err = asm.runAnalyzerManager(); err != nil { return } - var workingDirResults map[string]string - workingDirResults, err = a.getScanResults() - for cve, result := range workingDirResults { - a.applicabilityScanResults[cve] = result + workingDirResults, err := jas.ReadJasScanRunsFromFile(asm.scanner.ResultsFileName, wd) + if err != nil { + return } + asm.applicabilityScanResults = append(asm.applicabilityScanResults, workingDirResults...) return } -func (a *ApplicabilityScanManager) directDependenciesExist() bool { - return a.directDependenciesCves.Size() > 0 +func (asm *ApplicabilityScanManager) directDependenciesExist() bool { + return len(asm.directDependenciesCves) > 0 } -func (a *ApplicabilityScanManager) shouldRunApplicabilityScan(technologies []coreutils.Technology) bool { - return a.directDependenciesExist() && coreutils.ContainsApplicabilityScannableTech(technologies) +func (asm *ApplicabilityScanManager) shouldRunApplicabilityScan(technologies []coreutils.Technology) bool { + return asm.directDependenciesExist() && coreutils.ContainsApplicabilityScannableTech(technologies) } type applicabilityScanConfig struct { @@ -137,59 +139,24 @@ type scanConfiguration struct { SkippedDirs []string `yaml:"skipped-folders"` } -func (a *ApplicabilityScanManager) createConfigFile(workingDir string) error { +func (asm *ApplicabilityScanManager) createConfigFile(workingDir string) error { configFileContent := applicabilityScanConfig{ Scans: []scanConfiguration{ { Roots: []string{workingDir}, - Output: a.scanner.resultsFileName, + Output: asm.scanner.ResultsFileName, Type: applicabilityScanType, GrepDisable: false, - CveWhitelist: a.directDependenciesCves.ToSlice(), - SkippedDirs: skippedDirs, + CveWhitelist: asm.directDependenciesCves, + SkippedDirs: jas.SkippedDirs, }, }, } - return createScannersConfigFile(a.scanner.configFileName, configFileContent) + return jas.CreateScannersConfigFile(asm.scanner.ConfigFileName, configFileContent) } // Runs the analyzerManager app and returns a boolean to indicate whether the user is entitled for // advance security feature -func (a *ApplicabilityScanManager) runAnalyzerManager() error { - return a.scanner.analyzerManager.Exec(a.scanner.configFileName, applicabilityScanCommand, a.scanner.serverDetails) -} - -func (a *ApplicabilityScanManager) getScanResults() (map[string]string, error) { - report, err := sarif.Open(a.scanner.resultsFileName) - if errorutils.CheckError(err) != nil { - return nil, err - } - var fullVulnerabilitiesList []*sarif.Result - if len(report.Runs) > 0 { - fullVulnerabilitiesList = report.Runs[0].Results - } - - applicabilityScanResults := make(map[string]string) - for _, cve := range a.directDependenciesCves.ToSlice() { - applicabilityScanResults[cve] = utils.ApplicabilityUndeterminedStringValue - } - - for _, vulnerability := range fullVulnerabilitiesList { - applicableVulnerabilityName := getVulnerabilityName(*vulnerability.RuleID) - if isVulnerabilityApplicable(vulnerability) { - applicabilityScanResults[applicableVulnerabilityName] = utils.ApplicableStringValue - } else { - applicabilityScanResults[applicableVulnerabilityName] = utils.NotApplicableStringValue - } - } - return applicabilityScanResults, nil -} - -// Gets a result of one CVE from the scanner, and returns true if the CVE is applicable, false otherwise -func isVulnerabilityApplicable(result *sarif.Result) bool { - return !(result.Kind != nil && *result.Kind == "pass") -} - -func getVulnerabilityName(sarifRuleId string) string { - return strings.TrimPrefix(sarifRuleId, "applic_") +func (asm *ApplicabilityScanManager) runAnalyzerManager() error { + return asm.scanner.AnalyzerManager.Exec(asm.scanner.ConfigFileName, applicabilityScanCommand, filepath.Dir(asm.scanner.AnalyzerManager.AnalyzerManagerFullPath), asm.scanner.ServerDetails) } diff --git a/xray/commands/audit/jas/applicability/applicabilitymanager_test.go b/xray/commands/audit/jas/applicability/applicabilitymanager_test.go new file mode 100644 index 000000000..f887763c5 --- /dev/null +++ b/xray/commands/audit/jas/applicability/applicabilitymanager_test.go @@ -0,0 +1,319 @@ +package applicability + +import ( + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/jas" + "github.com/jfrog/jfrog-client-go/xray/services" + "github.com/stretchr/testify/assert" + "os" + "path/filepath" + "testing" +) + +var mockDirectDependencies = []string{"issueId_2_direct_dependency", "issueId_1_direct_dependency"} +var mockMultiRootDirectDependencies = []string{"issueId_2_direct_dependency", "issueId_1_direct_dependency", "issueId_3_direct_dependency", "issueId_4_direct_dependency"} + +func TestNewApplicabilityScanManager_InputIsValid(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + // Act + applicabilityManager := newApplicabilityScanManager(jas.FakeBasicXrayResults, mockDirectDependencies, scanner) + + // Assert + if assert.NotNil(t, applicabilityManager) { + assert.NotEmpty(t, applicabilityManager.scanner.ConfigFileName) + assert.NotEmpty(t, applicabilityManager.scanner.ResultsFileName) + assert.Len(t, applicabilityManager.directDependenciesCves, 5) + } +} + +func TestNewApplicabilityScanManager_DependencyTreeDoesntExist(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + // Act + applicabilityManager := newApplicabilityScanManager(jas.FakeBasicXrayResults, nil, scanner) + + // Assert + if assert.NotNil(t, applicabilityManager) { + assert.NotNil(t, applicabilityManager.scanner.ScannerDirCleanupFunc) + assert.Len(t, applicabilityManager.scanner.WorkingDirs, 1) + assert.NotEmpty(t, applicabilityManager.scanner.ConfigFileName) + assert.NotEmpty(t, applicabilityManager.scanner.ResultsFileName) + assert.Empty(t, applicabilityManager.directDependenciesCves) + } +} + +func TestNewApplicabilityScanManager_NoDirectDependenciesInScan(t *testing.T) { + // Arrange + var noDirectDependenciesResults = []services.ScanResponse{ + { + ScanId: "scanId_1", + Vulnerabilities: []services.Vulnerability{ + {IssueId: "issueId_1", Technology: coreutils.Pipenv.ToString(), + Cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}, {Id: "testCve3"}}, + Components: map[string]services.Component{ + "issueId_1_non_direct_dependency": {}}}, + }, + Violations: []services.Violation{ + {IssueId: "issueId_2", Technology: coreutils.Pipenv.ToString(), + Cves: []services.Cve{{Id: "testCve4"}, {Id: "testCve5"}}, + Components: map[string]services.Component{ + "issueId_2_non_direct_dependency": {}}}, + }, + }, + } + jas.FakeBasicXrayResults[0].Vulnerabilities[0].Components["issueId_1_non_direct_dependency"] = services.Component{} + jas.FakeBasicXrayResults[0].Violations[0].Components["issueId_2_non_direct_dependency"] = services.Component{} + + // Act + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + applicabilityManager := newApplicabilityScanManager(noDirectDependenciesResults, mockDirectDependencies, scanner) + + // Assert + if assert.NotNil(t, applicabilityManager) { + assert.NotEmpty(t, applicabilityManager.scanner.ConfigFileName) + assert.NotEmpty(t, applicabilityManager.scanner.ResultsFileName) + // Non-direct dependencies should not be added + assert.Empty(t, applicabilityManager.directDependenciesCves) + } +} + +func TestNewApplicabilityScanManager_MultipleDependencyTrees(t *testing.T) { + // Arrange + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + // Act + applicabilityManager := newApplicabilityScanManager(jas.FakeBasicXrayResults, mockMultiRootDirectDependencies, scanner) + + // Assert + if assert.NotNil(t, applicabilityManager) { + assert.NotEmpty(t, applicabilityManager.scanner.ConfigFileName) + assert.NotEmpty(t, applicabilityManager.scanner.ResultsFileName) + assert.Len(t, applicabilityManager.directDependenciesCves, 5) + } +} + +func TestNewApplicabilityScanManager_ViolationsDontExistInResults(t *testing.T) { + // Arrange + noViolationScanResponse := []services.ScanResponse{ + { + ScanId: "scanId_1", + Vulnerabilities: []services.Vulnerability{ + {IssueId: "issueId_1", Technology: coreutils.Pipenv.ToString(), + Cves: []services.Cve{{Id: "test_cve_1"}, {Id: "test_cve_2"}, {Id: "test_cve_3"}}, + Components: map[string]services.Component{"issueId_1_direct_dependency": {}}}, + }, + }, + } + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + + // Act + applicabilityManager := newApplicabilityScanManager(noViolationScanResponse, mockDirectDependencies, scanner) + + // Assert + if assert.NotNil(t, applicabilityManager) { + assert.NotEmpty(t, applicabilityManager.scanner.ConfigFileName) + assert.NotEmpty(t, applicabilityManager.scanner.ResultsFileName) + assert.Len(t, applicabilityManager.directDependenciesCves, 3) + } +} + +func TestNewApplicabilityScanManager_VulnerabilitiesDontExist(t *testing.T) { + // Arrange + noVulnerabilitiesScanResponse := []services.ScanResponse{ + { + ScanId: "scanId_1", + Violations: []services.Violation{ + {IssueId: "issueId_2", Technology: coreutils.Pipenv.ToString(), + Cves: []services.Cve{{Id: "test_cve_3"}, {Id: "test_cve_4"}}, + Components: map[string]services.Component{"issueId_2_direct_dependency": {}}}, + }, + }, + } + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + + // Act + applicabilityManager := newApplicabilityScanManager(noVulnerabilitiesScanResponse, mockDirectDependencies, scanner) + + // Assert + if assert.NotNil(t, applicabilityManager) { + assert.NotEmpty(t, applicabilityManager.scanner.ConfigFileName) + assert.NotEmpty(t, applicabilityManager.scanner.ResultsFileName) + assert.Len(t, applicabilityManager.directDependenciesCves, 2) + } +} + +func TestApplicabilityScanManager_ShouldRun_TechnologiesNotEligibleForScan(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + + results, err := RunApplicabilityScan(jas.FakeBasicXrayResults, mockDirectDependencies, + []coreutils.Technology{coreutils.Nuget, coreutils.Go}, scanner) + + // Assert + assert.Nil(t, results) + assert.NoError(t, err) +} + +func TestApplicabilityScanManager_ShouldRun_ScanResultsAreEmpty(t *testing.T) { + // Arrange + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + + applicabilityManager := newApplicabilityScanManager(nil, mockDirectDependencies, scanner) + + // Assert + eligible := applicabilityManager.shouldRunApplicabilityScan([]coreutils.Technology{coreutils.Npm}) + assert.False(t, eligible) +} + +func TestExtractXrayDirectViolations(t *testing.T) { + var xrayResponseForDirectViolationsTest = []services.ScanResponse{ + { + Violations: []services.Violation{ + {IssueId: "issueId_2", Technology: coreutils.Pipenv.ToString(), + Cves: []services.Cve{{Id: "testCve4"}, {Id: "testCve5"}}, + Components: map[string]services.Component{"issueId_2_direct_dependency": {}}}, + }, + }, + } + tests := []struct { + directDependencies []string + cvesCount int + }{ + {directDependencies: []string{"issueId_2_direct_dependency", "issueId_1_direct_dependency"}, + cvesCount: 2, + }, + // Vulnerability dependency, should be ignored by function + {directDependencies: []string{"issueId_1_direct_dependency"}, + cvesCount: 0, + }, + {directDependencies: []string{}, + cvesCount: 0, + }, + } + + for _, test := range tests { + cves := extractDirectDependenciesCvesFromScan(xrayResponseForDirectViolationsTest, test.directDependencies) + assert.Len(t, cves, test.cvesCount) + } +} + +func TestExtractXrayDirectVulnerabilities(t *testing.T) { + var xrayResponseForDirectVulnerabilitiesTest = []services.ScanResponse{ + { + ScanId: "scanId_1", + Vulnerabilities: []services.Vulnerability{ + { + IssueId: "issueId_1", Technology: coreutils.Pipenv.ToString(), + Cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}, {Id: "testCve3"}}, + Components: map[string]services.Component{"issueId_1_direct_dependency": {}}, + }, + { + IssueId: "issueId_2", Technology: coreutils.Pipenv.ToString(), + Cves: []services.Cve{{Id: "testCve4"}, {Id: "testCve5"}}, + Components: map[string]services.Component{"issueId_2_direct_dependency": {}}, + }, + }, + }, + } + tests := []struct { + directDependencies []string + cvesCount int + }{ + { + directDependencies: []string{"issueId_1_direct_dependency"}, + cvesCount: 3, + }, + { + directDependencies: []string{"issueId_2_direct_dependency"}, + cvesCount: 2, + }, + {directDependencies: []string{}, + cvesCount: 0, + }, + } + + for _, test := range tests { + assert.Len(t, extractDirectDependenciesCvesFromScan(xrayResponseForDirectVulnerabilitiesTest, test.directDependencies), test.cvesCount) + } +} + +func TestCreateConfigFile_VerifyFileWasCreated(t *testing.T) { + // Arrange + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + + applicabilityManager := newApplicabilityScanManager(jas.FakeBasicXrayResults, []string{"issueId_1_direct_dependency", "issueId_2_direct_dependency"}, scanner) + + currWd, err := coreutils.GetWorkingDirectory() + assert.NoError(t, err) + err = applicabilityManager.createConfigFile(currWd) + assert.NoError(t, err) + + defer func() { + err = os.Remove(applicabilityManager.scanner.ConfigFileName) + assert.NoError(t, err) + }() + + _, fileNotExistError := os.Stat(applicabilityManager.scanner.ConfigFileName) + assert.NoError(t, fileNotExistError) + fileContent, err := os.ReadFile(applicabilityManager.scanner.ConfigFileName) + assert.NoError(t, err) + assert.True(t, len(fileContent) > 0) +} + +func TestParseResults_EmptyResults_AllCvesShouldGetUnknown(t *testing.T) { + // Arrange + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + + applicabilityManager := newApplicabilityScanManager(jas.FakeBasicXrayResults, mockDirectDependencies, scanner) + applicabilityManager.scanner.ResultsFileName = filepath.Join(jas.GetTestDataPath(), "applicability-scan", "empty-results.sarif") + + // Act + var err error + applicabilityManager.applicabilityScanResults, err = jas.ReadJasScanRunsFromFile(applicabilityManager.scanner.ResultsFileName, scanner.WorkingDirs[0]) + + if assert.NoError(t, err) { + assert.Len(t, applicabilityManager.applicabilityScanResults, 1) + assert.Empty(t, applicabilityManager.applicabilityScanResults[0].Results) + } +} + +func TestParseResults_ApplicableCveExist(t *testing.T) { + // Arrange + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + applicabilityManager := newApplicabilityScanManager(jas.FakeBasicXrayResults, mockDirectDependencies, scanner) + applicabilityManager.scanner.ResultsFileName = filepath.Join(jas.GetTestDataPath(), "applicability-scan", "applicable-cve-results.sarif") + + // Act + var err error + applicabilityManager.applicabilityScanResults, err = jas.ReadJasScanRunsFromFile(applicabilityManager.scanner.ResultsFileName, scanner.WorkingDirs[0]) + + if assert.NoError(t, err) && assert.NotNil(t, applicabilityManager.applicabilityScanResults) { + assert.Len(t, applicabilityManager.applicabilityScanResults, 1) + assert.NotEmpty(t, applicabilityManager.applicabilityScanResults[0].Results) + } +} + +func TestParseResults_AllCvesNotApplicable(t *testing.T) { + // Arrange + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + applicabilityManager := newApplicabilityScanManager(jas.FakeBasicXrayResults, mockDirectDependencies, scanner) + applicabilityManager.scanner.ResultsFileName = filepath.Join(jas.GetTestDataPath(), "applicability-scan", "no-applicable-cves-results.sarif") + + // Act + var err error + applicabilityManager.applicabilityScanResults, err = jas.ReadJasScanRunsFromFile(applicabilityManager.scanner.ResultsFileName, scanner.WorkingDirs[0]) + + if assert.NoError(t, err) && assert.NotNil(t, applicabilityManager.applicabilityScanResults) { + assert.Len(t, applicabilityManager.applicabilityScanResults, 1) + assert.NotEmpty(t, applicabilityManager.applicabilityScanResults[0].Results) + } +} diff --git a/xray/commands/audit/jas/common.go b/xray/commands/audit/jas/common.go new file mode 100644 index 000000000..3814e06d0 --- /dev/null +++ b/xray/commands/audit/jas/common.go @@ -0,0 +1,194 @@ +package jas + +import ( + "errors" + "os" + "path/filepath" + "strings" + "testing" + + rtutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" + "github.com/jfrog/jfrog-cli-core/v2/utils/config" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-cli-core/v2/xray/utils" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + "github.com/jfrog/jfrog-client-go/utils/io/fileutils" + "github.com/jfrog/jfrog-client-go/xray/services" + "github.com/owenrumney/go-sarif/v2/sarif" + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" +) + +var ( + SkippedDirs = []string{"**/*test*/**", "**/*venv*/**", "**/*node_modules*/**", "**/*target*/**"} + + mapSeverityToScore = map[string]string{ + "": "0.0", + "unknown": "0.0", + "low": "3.9", + "medium": "6.9", + "high": "8.9", + "critical": "10", + } +) + +type JasScanner struct { + ConfigFileName string + ResultsFileName string + AnalyzerManager utils.AnalyzerManager + ServerDetails *config.ServerDetails + WorkingDirs []string + ScannerDirCleanupFunc func() error +} + +func NewJasScanner(workingDirs []string, serverDetails *config.ServerDetails, multiScanId string) (scanner *JasScanner, err error) { + scanner = &JasScanner{} + if scanner.AnalyzerManager.AnalyzerManagerFullPath, err = utils.GetAnalyzerManagerExecutable(); err != nil { + return + } + var tempDir string + if tempDir, err = fileutils.CreateTempDir(); err != nil { + return + } + scanner.ScannerDirCleanupFunc = func() error { + return fileutils.RemoveTempDir(tempDir) + } + scanner.ServerDetails = serverDetails + scanner.ConfigFileName = filepath.Join(tempDir, "config.yaml") + scanner.ResultsFileName = filepath.Join(tempDir, "results.sarif") + scanner.WorkingDirs, err = coreutils.GetFullPathsWorkingDirs(workingDirs) + scanner.AnalyzerManager.MultiScanId = multiScanId + return +} + +type ScannerCmd interface { + Run(wd string) (err error) +} + +func (a *JasScanner) Run(scannerCmd ScannerCmd) (err error) { + for _, workingDir := range a.WorkingDirs { + func() { + defer func() { + err = errors.Join(err, deleteJasProcessFiles(a.ConfigFileName, a.ResultsFileName)) + }() + if err = scannerCmd.Run(workingDir); err != nil { + return + } + }() + } + return +} + +func deleteJasProcessFiles(configFile string, resultFile string) error { + exist, err := fileutils.IsFileExists(configFile, false) + if err != nil { + return err + } + if exist { + if err = os.Remove(configFile); err != nil { + return errorutils.CheckError(err) + } + } + exist, err = fileutils.IsFileExists(resultFile, false) + if err != nil { + return err + } + if exist { + err = os.Remove(resultFile) + } + return errorutils.CheckError(err) +} + +func ReadJasScanRunsFromFile(fileName, wd string) (sarifRuns []*sarif.Run, err error) { + if sarifRuns, err = utils.ReadScanRunsFromFile(fileName); err != nil { + return + } + for _, sarifRun := range sarifRuns { + // Jas reports has only one invocation + // Set the actual working directory to the invocation, not the analyzerManager directory + // Also used to calculate relative paths if needed with it + sarifRun.Invocations[0].WorkingDirectory.WithUri(wd) + // Process runs values + sarifRun.Results = excludeSuppressResults(sarifRun.Results) + addScoreToRunRules(sarifRun) + } + return +} + +func excludeSuppressResults(sarifResults []*sarif.Result) []*sarif.Result { + results := []*sarif.Result{} + for _, sarifResult := range sarifResults { + if len(sarifResult.Suppressions) > 0 { + // Describes a request to “suppress” a result (to exclude it from result lists) + continue + } + results = append(results, sarifResult) + } + return results +} + +func addScoreToRunRules(sarifRun *sarif.Run) { + for _, sarifResult := range sarifRun.Results { + if rule, err := sarifRun.GetRuleById(*sarifResult.RuleID); err == nil { + // Add to the rule security-severity score based on results severity + score := convertToScore(utils.GetResultSeverity(sarifResult)) + if score != utils.MissingCveScore { + if rule.Properties == nil { + rule.WithProperties(sarif.NewPropertyBag().Properties) + } + rule.Properties["security-severity"] = score + } + } + } +} + +func convertToScore(severity string) string { + if level, ok := mapSeverityToScore[strings.ToLower(severity)]; ok { + return level + } + return "" +} + +func CreateScannersConfigFile(fileName string, fileContent interface{}) error { + yamlData, err := yaml.Marshal(&fileContent) + if errorutils.CheckError(err) != nil { + return err + } + err = os.WriteFile(fileName, yamlData, 0644) + return errorutils.CheckError(err) +} + +var FakeServerDetails = config.ServerDetails{ + Url: "platformUrl", + Password: "password", + User: "user", +} + +var FakeBasicXrayResults = []services.ScanResponse{ + { + ScanId: "scanId_1", + Vulnerabilities: []services.Vulnerability{ + {IssueId: "issueId_1", Technology: coreutils.Pipenv.ToString(), + Cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}, {Id: "testCve3"}}, + Components: map[string]services.Component{"issueId_1_direct_dependency": {}, "issueId_3_direct_dependency": {}}}, + }, + Violations: []services.Violation{ + {IssueId: "issueId_2", Technology: coreutils.Pipenv.ToString(), + Cves: []services.Cve{{Id: "testCve4"}, {Id: "testCve5"}}, + Components: map[string]services.Component{"issueId_2_direct_dependency": {}, "issueId_4_direct_dependency": {}}}, + }, + }, +} + +func InitJasTest(t *testing.T, workingDirs ...string) (*JasScanner, func()) { + assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) + scanner, err := NewJasScanner(workingDirs, &FakeServerDetails, "") + assert.NoError(t, err) + return scanner, func() { + assert.NoError(t, scanner.ScannerDirCleanupFunc()) + } +} + +func GetTestDataPath() string { + return filepath.Join("..", "..", "..", "testdata") +} diff --git a/xray/audit/jas/iacscanner.go b/xray/commands/audit/jas/iac/iacscanner.go similarity index 63% rename from xray/audit/jas/iacscanner.go rename to xray/commands/audit/jas/iac/iacscanner.go index 629cd384f..c4ccfdd39 100644 --- a/xray/audit/jas/iacscanner.go +++ b/xray/commands/audit/jas/iac/iacscanner.go @@ -1,8 +1,12 @@ -package jas +package iac import ( + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/jas" + "path/filepath" + "github.com/jfrog/jfrog-cli-core/v2/xray/utils" "github.com/jfrog/jfrog-client-go/utils/log" + "github.com/owenrumney/go-sarif/v2/sarif" ) const ( @@ -11,8 +15,8 @@ const ( ) type IacScanManager struct { - iacScannerResults []utils.IacOrSecretResult - scanner *AdvancedSecurityScanner + iacScannerResults []*sarif.Run + scanner *jas.JasScanner } // The getIacScanResults function runs the iac scan flow, which includes the following steps: @@ -20,10 +24,10 @@ type IacScanManager struct { // Running the analyzer manager executable. // Parsing the analyzer manager results. // Return values: -// []utils.IacOrSecretResult: a list of the iac violations that were found. +// []utils.SourceCodeScanResult: a list of the iac violations that were found. // bool: true if the user is entitled to iac scan, false otherwise. // error: An error object (if any). -func getIacScanResults(scanner *AdvancedSecurityScanner) (results []utils.IacOrSecretResult, err error) { +func RunIacScan(scanner *jas.JasScanner) (results []*sarif.Run, err error) { iacScanManager := newIacScanManager(scanner) log.Info("Running IaC scanning...") if err = iacScanManager.scanner.Run(iacScanManager); err != nil { @@ -31,15 +35,15 @@ func getIacScanResults(scanner *AdvancedSecurityScanner) (results []utils.IacOrS return } if len(iacScanManager.iacScannerResults) > 0 { - log.Info("Found", len(iacScanManager.iacScannerResults), "IaC vulnerabilities") + log.Info("Found", utils.GetResultsLocationCount(iacScanManager.iacScannerResults...), "IaC vulnerabilities") } results = iacScanManager.iacScannerResults return } -func newIacScanManager(scanner *AdvancedSecurityScanner) (manager *IacScanManager) { +func newIacScanManager(scanner *jas.JasScanner) (manager *IacScanManager) { return &IacScanManager{ - iacScannerResults: []utils.IacOrSecretResult{}, + iacScannerResults: []*sarif.Run{}, scanner: scanner, } } @@ -52,8 +56,10 @@ func (iac *IacScanManager) Run(wd string) (err error) { if err = iac.runAnalyzerManager(); err != nil { return } - var workingDirResults []utils.IacOrSecretResult - workingDirResults, err = getIacOrSecretsScanResults(scanner.resultsFileName, wd, false) + workingDirResults, err := jas.ReadJasScanRunsFromFile(scanner.ResultsFileName, wd) + if err != nil { + return + } iac.iacScannerResults = append(iac.iacScannerResults, workingDirResults...) return } @@ -74,15 +80,15 @@ func (iac *IacScanManager) createConfigFile(currentWd string) error { Scans: []iacScanConfiguration{ { Roots: []string{currentWd}, - Output: iac.scanner.resultsFileName, + Output: iac.scanner.ResultsFileName, Type: iacScannerType, - SkippedDirs: skippedDirs, + SkippedDirs: jas.SkippedDirs, }, }, } - return createScannersConfigFile(iac.scanner.configFileName, configFileContent) + return jas.CreateScannersConfigFile(iac.scanner.ConfigFileName, configFileContent) } func (iac *IacScanManager) runAnalyzerManager() error { - return iac.scanner.analyzerManager.Exec(iac.scanner.configFileName, iacScanCommand, iac.scanner.serverDetails) + return iac.scanner.AnalyzerManager.Exec(iac.scanner.ConfigFileName, iacScanCommand, filepath.Dir(iac.scanner.AnalyzerManager.AnalyzerManagerFullPath), iac.scanner.ServerDetails) } diff --git a/xray/commands/audit/jas/iac/iacscanner_test.go b/xray/commands/audit/jas/iac/iacscanner_test.go new file mode 100644 index 000000000..a2332421d --- /dev/null +++ b/xray/commands/audit/jas/iac/iacscanner_test.go @@ -0,0 +1,81 @@ +package iac + +import ( + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/jas" + "os" + "path/filepath" + "testing" + + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/stretchr/testify/assert" +) + +func TestNewIacScanManager(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t, "currentDir") + defer cleanUp() + // Act + iacScanManager := newIacScanManager(scanner) + + // Assert + if assert.NotNil(t, iacScanManager) { + assert.NotEmpty(t, iacScanManager.scanner.ConfigFileName) + assert.NotEmpty(t, iacScanManager.scanner.ResultsFileName) + assert.NotEmpty(t, iacScanManager.scanner.WorkingDirs) + assert.Equal(t, &jas.FakeServerDetails, iacScanManager.scanner.ServerDetails) + } +} + +func TestIacScan_CreateConfigFile_VerifyFileWasCreated(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t, "currentDir") + defer cleanUp() + + iacScanManager := newIacScanManager(scanner) + + currWd, err := coreutils.GetWorkingDirectory() + assert.NoError(t, err) + err = iacScanManager.createConfigFile(currWd) + + defer func() { + err = os.Remove(iacScanManager.scanner.ConfigFileName) + assert.NoError(t, err) + }() + + _, fileNotExistError := os.Stat(iacScanManager.scanner.ConfigFileName) + assert.NoError(t, fileNotExistError) + fileContent, err := os.ReadFile(iacScanManager.scanner.ConfigFileName) + assert.NoError(t, err) + assert.True(t, len(fileContent) > 0) +} + +func TestIacParseResults_EmptyResults(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + + // Arrange + iacScanManager := newIacScanManager(scanner) + iacScanManager.scanner.ResultsFileName = filepath.Join(jas.GetTestDataPath(), "iac-scan", "no-violations.sarif") + + // Act + var err error + iacScanManager.iacScannerResults, err = jas.ReadJasScanRunsFromFile(iacScanManager.scanner.ResultsFileName, scanner.WorkingDirs[0]) + if assert.NoError(t, err) && assert.NotNil(t, iacScanManager.iacScannerResults) { + assert.Len(t, iacScanManager.iacScannerResults, 1) + assert.Empty(t, iacScanManager.iacScannerResults[0].Results) + } +} + +func TestIacParseResults_ResultsContainIacViolations(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + // Arrange + iacScanManager := newIacScanManager(scanner) + iacScanManager.scanner.ResultsFileName = filepath.Join(jas.GetTestDataPath(), "iac-scan", "contains-iac-violations.sarif") + + // Act + var err error + iacScanManager.iacScannerResults, err = jas.ReadJasScanRunsFromFile(iacScanManager.scanner.ResultsFileName, scanner.WorkingDirs[0]) + if assert.NoError(t, err) && assert.NotNil(t, iacScanManager.iacScannerResults) { + assert.Len(t, iacScanManager.iacScannerResults, 1) + assert.Len(t, iacScanManager.iacScannerResults[0].Results, 4) + } +} diff --git a/xray/commands/audit/jas/sast/sastscanner.go b/xray/commands/audit/jas/sast/sastscanner.go new file mode 100644 index 000000000..35211396f --- /dev/null +++ b/xray/commands/audit/jas/sast/sastscanner.go @@ -0,0 +1,95 @@ +package sast + +import ( + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/jas" + "github.com/jfrog/jfrog-cli-core/v2/xray/utils" + "github.com/jfrog/jfrog-client-go/utils/log" + "github.com/owenrumney/go-sarif/v2/sarif" + "golang.org/x/exp/maps" +) + +const ( + sastScanCommand = "zd" +) + +type SastScanManager struct { + sastScannerResults []*sarif.Run + scanner *jas.JasScanner +} + +func RunSastScan(scanner *jas.JasScanner) (results []*sarif.Run, err error) { + sastScanManager := newSastScanManager(scanner) + log.Info("Running SAST scanning...") + if err = sastScanManager.scanner.Run(sastScanManager); err != nil { + err = utils.ParseAnalyzerManagerError(utils.Sast, err) + return + } + if len(sastScanManager.sastScannerResults) > 0 { + log.Info("Found", utils.GetResultsLocationCount(sastScanManager.sastScannerResults...), "SAST vulnerabilities") + } + results = sastScanManager.sastScannerResults + return +} + +func newSastScanManager(scanner *jas.JasScanner) (manager *SastScanManager) { + return &SastScanManager{ + sastScannerResults: []*sarif.Run{}, + scanner: scanner, + } +} + +func (ssm *SastScanManager) Run(wd string) (err error) { + scanner := ssm.scanner + if err = ssm.runAnalyzerManager(wd); err != nil { + return + } + workingDirRuns, err := jas.ReadJasScanRunsFromFile(scanner.ResultsFileName, wd) + if err != nil { + return + } + ssm.sastScannerResults = append(ssm.sastScannerResults, groupResultsByLocation(workingDirRuns)...) + return +} + +func (ssm *SastScanManager) runAnalyzerManager(wd string) error { + return ssm.scanner.AnalyzerManager.Exec(ssm.scanner.ResultsFileName, sastScanCommand, wd, ssm.scanner.ServerDetails) +} + +// In the Sast scanner, there can be multiple results with the same location. +// The only difference is that their CodeFlow values are different. +// We combine those under the same result location value +func groupResultsByLocation(sarifRuns []*sarif.Run) []*sarif.Run { + for _, sastRun := range sarifRuns { + locationToResult := map[string]*sarif.Result{} + for _, sastResult := range sastRun.Results { + resultID := getResultId(sastResult) + if result, exists := locationToResult[resultID]; exists { + result.CodeFlows = append(result.CodeFlows, sastResult.CodeFlows...) + } else { + locationToResult[resultID] = sastResult + } + } + sastRun.Results = maps.Values(locationToResult) + } + return sarifRuns +} + +// In Sast there is only one location for each result +func getResultFileName(result *sarif.Result) string { + if len(result.Locations) > 0 { + return utils.GetLocationFileName(result.Locations[0]) + } + return "" +} + +// In Sast there is only one location for each result +func getResultStartLocationInFile(result *sarif.Result) string { + if len(result.Locations) > 0 { + return utils.GetStartLocationInFile(result.Locations[0]) + } + return "" +} + +func getResultId(result *sarif.Result) string { + return getResultFileName(result) + getResultStartLocationInFile(result) + utils.GetResultSeverity(result) + utils.GetResultMsgText(result) +} diff --git a/xray/commands/audit/jas/sast/sastscanner_test.go b/xray/commands/audit/jas/sast/sastscanner_test.go new file mode 100644 index 000000000..66c423403 --- /dev/null +++ b/xray/commands/audit/jas/sast/sastscanner_test.go @@ -0,0 +1,68 @@ +package sast + +import ( + "path/filepath" + "testing" + + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/jas" + + "github.com/stretchr/testify/assert" +) + +func TestNewSastScanManager(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t, "currentDir") + defer cleanUp() + // Act + sastScanManager := newSastScanManager(scanner) + + // Assert + if assert.NotNil(t, sastScanManager) { + assert.NotEmpty(t, sastScanManager.scanner.ConfigFileName) + assert.NotEmpty(t, sastScanManager.scanner.ResultsFileName) + assert.NotEmpty(t, sastScanManager.scanner.WorkingDirs) + assert.Equal(t, &jas.FakeServerDetails, sastScanManager.scanner.ServerDetails) + } +} + +func TestSastParseResults_EmptyResults(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + + // Arrange + sastScanManager := newSastScanManager(scanner) + sastScanManager.scanner.ResultsFileName = filepath.Join(jas.GetTestDataPath(), "sast-scan", "no-violations.sarif") + + // Act + var err error + sastScanManager.sastScannerResults, err = jas.ReadJasScanRunsFromFile(sastScanManager.scanner.ResultsFileName, scanner.WorkingDirs[0]) + + // Assert + if assert.NoError(t, err) && assert.NotNil(t, sastScanManager.sastScannerResults) { + assert.Len(t, sastScanManager.sastScannerResults, 1) + assert.Empty(t, sastScanManager.sastScannerResults[0].Results) + sastScanManager.sastScannerResults = groupResultsByLocation(sastScanManager.sastScannerResults) + assert.Len(t, sastScanManager.sastScannerResults, 1) + assert.Empty(t, sastScanManager.sastScannerResults[0].Results) + } +} + +func TestSastParseResults_ResultsContainIacViolations(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + // Arrange + sastScanManager := newSastScanManager(scanner) + sastScanManager.scanner.ResultsFileName = filepath.Join(jas.GetTestDataPath(), "sast-scan", "contains-sast-violations.sarif") + + // Act + var err error + sastScanManager.sastScannerResults, err = jas.ReadJasScanRunsFromFile(sastScanManager.scanner.ResultsFileName, scanner.WorkingDirs[0]) + + // Assert + if assert.NoError(t, err) && assert.NotNil(t, sastScanManager.sastScannerResults) { + assert.Len(t, sastScanManager.sastScannerResults, 1) + assert.NotEmpty(t, sastScanManager.sastScannerResults[0].Results) + sastScanManager.sastScannerResults = groupResultsByLocation(sastScanManager.sastScannerResults) + // File has 4 results, 2 of them at the same location different codeFlow + assert.Len(t, sastScanManager.sastScannerResults[0].Results, 3) + } +} diff --git a/xray/audit/jas/secretsscanner.go b/xray/commands/audit/jas/secrets/secretsscanner.go similarity index 55% rename from xray/audit/jas/secretsscanner.go rename to xray/commands/audit/jas/secrets/secretsscanner.go index cd1159a49..cf5df05f8 100644 --- a/xray/audit/jas/secretsscanner.go +++ b/xray/commands/audit/jas/secrets/secretsscanner.go @@ -1,10 +1,13 @@ -package jas +package secrets import ( + "path/filepath" "strings" + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/jas" "github.com/jfrog/jfrog-cli-core/v2/xray/utils" "github.com/jfrog/jfrog-client-go/utils/log" + "github.com/owenrumney/go-sarif/v2/sarif" ) const ( @@ -13,8 +16,8 @@ const ( ) type SecretScanManager struct { - secretsScannerResults []utils.IacOrSecretResult - scanner *AdvancedSecurityScanner + secretsScannerResults []*sarif.Run + scanner *jas.JasScanner } // The getSecretsScanResults function runs the secrets scan flow, which includes the following steps: @@ -24,7 +27,7 @@ type SecretScanManager struct { // Return values: // []utils.IacOrSecretResult: a list of the secrets that were found. // error: An error object (if any). -func getSecretsScanResults(scanner *AdvancedSecurityScanner) (results []utils.IacOrSecretResult, err error) { +func RunSecretsScan(scanner *jas.JasScanner) (results []*sarif.Run, err error) { secretScanManager := newSecretsScanManager(scanner) log.Info("Running secrets scanning...") if err = secretScanManager.scanner.Run(secretScanManager); err != nil { @@ -33,14 +36,14 @@ func getSecretsScanResults(scanner *AdvancedSecurityScanner) (results []utils.Ia } results = secretScanManager.secretsScannerResults if len(results) > 0 { - log.Info(len(results), "secrets were found") + log.Info("Found", utils.GetResultsLocationCount(results...), "secrets") } return } -func newSecretsScanManager(scanner *AdvancedSecurityScanner) (manager *SecretScanManager) { +func newSecretsScanManager(scanner *jas.JasScanner) (manager *SecretScanManager) { return &SecretScanManager{ - secretsScannerResults: []utils.IacOrSecretResult{}, + secretsScannerResults: []*sarif.Run{}, scanner: scanner, } } @@ -53,9 +56,11 @@ func (s *SecretScanManager) Run(wd string) (err error) { if err = s.runAnalyzerManager(); err != nil { return } - var workingDirResults []utils.IacOrSecretResult - workingDirResults, err = getIacOrSecretsScanResults(scanner.resultsFileName, wd, true) - s.secretsScannerResults = append(s.secretsScannerResults, workingDirResults...) + workingDirRuns, err := jas.ReadJasScanRunsFromFile(scanner.ResultsFileName, wd) + if err != nil { + return + } + s.secretsScannerResults = append(s.secretsScannerResults, processSecretScanRuns(workingDirRuns)...) return } @@ -75,22 +80,35 @@ func (s *SecretScanManager) createConfigFile(currentWd string) error { Scans: []secretsScanConfiguration{ { Roots: []string{currentWd}, - Output: s.scanner.resultsFileName, + Output: s.scanner.ResultsFileName, Type: secretsScannerType, - SkippedDirs: skippedDirs, + SkippedDirs: jas.SkippedDirs, }, }, } - return createScannersConfigFile(s.scanner.configFileName, configFileContent) + return jas.CreateScannersConfigFile(s.scanner.ConfigFileName, configFileContent) } func (s *SecretScanManager) runAnalyzerManager() error { - return s.scanner.analyzerManager.Exec(s.scanner.configFileName, secretsScanCommand, s.scanner.serverDetails) + return s.scanner.AnalyzerManager.Exec(s.scanner.ConfigFileName, secretsScanCommand, filepath.Dir(s.scanner.AnalyzerManager.AnalyzerManagerFullPath), s.scanner.ServerDetails) } -func hideSecret(secret string) string { +func maskSecret(secret string) string { if len(secret) <= 3 { return "***" } return secret[:3] + strings.Repeat("*", 12) } + +func processSecretScanRuns(sarifRuns []*sarif.Run) []*sarif.Run { + for _, secretRun := range sarifRuns { + // Hide discovered secrets value + for _, secretResult := range secretRun.Results { + for _, location := range secretResult.Locations { + secret := utils.GetLocationSnippetPointer(location) + utils.SetLocationSnippet(location, maskSecret(*secret)) + } + } + } + return sarifRuns +} diff --git a/xray/commands/audit/jas/secrets/secretsscanner_test.go b/xray/commands/audit/jas/secrets/secretsscanner_test.go new file mode 100644 index 000000000..14e917e16 --- /dev/null +++ b/xray/commands/audit/jas/secrets/secretsscanner_test.go @@ -0,0 +1,131 @@ +package secrets + +import ( + "os" + "path/filepath" + "testing" + + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/jas" + + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/stretchr/testify/assert" +) + +func TestNewSecretsScanManager(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + secretScanManager := newSecretsScanManager(scanner) + + assert.NotEmpty(t, secretScanManager) + assert.NotEmpty(t, secretScanManager.scanner.ConfigFileName) + assert.NotEmpty(t, secretScanManager.scanner.ResultsFileName) + assert.Equal(t, &jas.FakeServerDetails, secretScanManager.scanner.ServerDetails) +} + +func TestSecretsScan_CreateConfigFile_VerifyFileWasCreated(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + secretScanManager := newSecretsScanManager(scanner) + + currWd, err := coreutils.GetWorkingDirectory() + assert.NoError(t, err) + err = secretScanManager.createConfigFile(currWd) + assert.NoError(t, err) + + defer func() { + err = os.Remove(secretScanManager.scanner.ConfigFileName) + assert.NoError(t, err) + }() + + _, fileNotExistError := os.Stat(secretScanManager.scanner.ConfigFileName) + assert.NoError(t, fileNotExistError) + fileContent, err := os.ReadFile(secretScanManager.scanner.ConfigFileName) + assert.NoError(t, err) + assert.True(t, len(fileContent) > 0) +} + +func TestRunAnalyzerManager_ReturnsGeneralError(t *testing.T) { + defer func() { + os.Clearenv() + }() + + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + + secretScanManager := newSecretsScanManager(scanner) + assert.Error(t, secretScanManager.runAnalyzerManager()) +} + +func TestParseResults_EmptyResults(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + // Arrange + secretScanManager := newSecretsScanManager(scanner) + secretScanManager.scanner.ResultsFileName = filepath.Join(jas.GetTestDataPath(), "secrets-scan", "no-secrets.sarif") + + // Act + var err error + secretScanManager.secretsScannerResults, err = jas.ReadJasScanRunsFromFile(secretScanManager.scanner.ResultsFileName, scanner.WorkingDirs[0]) + + // Assert + if assert.NoError(t, err) && assert.NotNil(t, secretScanManager.secretsScannerResults) { + assert.Len(t, secretScanManager.secretsScannerResults, 1) + assert.Empty(t, secretScanManager.secretsScannerResults[0].Results) + secretScanManager.secretsScannerResults = processSecretScanRuns(secretScanManager.secretsScannerResults) + assert.Len(t, secretScanManager.secretsScannerResults, 1) + assert.Empty(t, secretScanManager.secretsScannerResults[0].Results) + } + +} + +func TestParseResults_ResultsContainSecrets(t *testing.T) { + // Arrange + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + + secretScanManager := newSecretsScanManager(scanner) + secretScanManager.scanner.ResultsFileName = filepath.Join(jas.GetTestDataPath(), "secrets-scan", "contain-secrets.sarif") + + // Act + var err error + secretScanManager.secretsScannerResults, err = jas.ReadJasScanRunsFromFile(secretScanManager.scanner.ResultsFileName, scanner.WorkingDirs[0]) + + // Assert + if assert.NoError(t, err) && assert.NotNil(t, secretScanManager.secretsScannerResults) { + assert.Len(t, secretScanManager.secretsScannerResults, 1) + assert.NotEmpty(t, secretScanManager.secretsScannerResults[0].Results) + secretScanManager.secretsScannerResults = processSecretScanRuns(secretScanManager.secretsScannerResults) + assert.Len(t, secretScanManager.secretsScannerResults, 1) + assert.Len(t, secretScanManager.secretsScannerResults[0].Results, 7) + } + assert.NoError(t, err) + +} + +func TestGetSecretsScanResults_AnalyzerManagerReturnsError(t *testing.T) { + scanner, cleanUp := jas.InitJasTest(t) + defer cleanUp() + + secretsResults, err := RunSecretsScan(scanner) + + assert.Error(t, err) + assert.ErrorContains(t, err, "failed to run Secrets scan") + assert.Nil(t, secretsResults) +} + +func TestHideSecret(t *testing.T) { + tests := []struct { + secret string + expectedOutput string + }{ + {secret: "", expectedOutput: "***"}, + {secret: "12", expectedOutput: "***"}, + {secret: "123", expectedOutput: "***"}, + {secret: "123456789", expectedOutput: "123************"}, + {secret: "3478hfnkjhvd848446gghgfh", expectedOutput: "347************"}, + } + + for _, test := range tests { + assert.Equal(t, test.expectedOutput, maskSecret(test.secret)) + } +} diff --git a/xray/commands/audit/jasrunner.go b/xray/commands/audit/jasrunner.go new file mode 100644 index 000000000..9f917004b --- /dev/null +++ b/xray/commands/audit/jasrunner.go @@ -0,0 +1,59 @@ +package audit + +import ( + "errors" + "github.com/jfrog/jfrog-cli-core/v2/utils/config" + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/jas" + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/jas/applicability" + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/jas/iac" + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/jas/sast" + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/jas/secrets" + "github.com/jfrog/jfrog-cli-core/v2/xray/utils" + "github.com/jfrog/jfrog-client-go/utils/io" + "github.com/jfrog/jfrog-client-go/utils/log" +) + +func runJasScannersAndSetResults(scanResults *utils.ExtendedScanResults, directDependencies []string, + serverDetails *config.ServerDetails, workingDirs []string, progress io.ProgressMgr, multiScanId string) (err error) { + if serverDetails == nil || len(serverDetails.Url) == 0 { + log.Warn("To include 'Advanced Security' scan as part of the audit output, please run the 'jf c add' command before running this command.") + return + } + scanner, err := jas.NewJasScanner(workingDirs, serverDetails, multiScanId) + if err != nil { + return + } + defer func() { + cleanup := scanner.ScannerDirCleanupFunc + err = errors.Join(err, cleanup()) + }() + if progress != nil { + progress.SetHeadlineMsg("Running applicability scanning") + } + scanResults.ApplicabilityScanResults, err = applicability.RunApplicabilityScan(scanResults.XrayResults, directDependencies, scanResults.ScannedTechnologies, scanner) + if err != nil { + return + } + if progress != nil { + progress.SetHeadlineMsg("Running secrets scanning") + } + scanResults.SecretsScanResults, err = secrets.RunSecretsScan(scanner) + if err != nil { + return + } + if progress != nil { + progress.SetHeadlineMsg("Running IaC scanning") + } + scanResults.IacScanResults, err = iac.RunIacScan(scanner) + if err != nil { + return + } + if !utils.IsSastSupported() { + return + } + if progress != nil { + progress.SetHeadlineMsg("Running SAST scanning") + } + scanResults.SastScanResults, err = sast.RunSastScan(scanner) + return +} diff --git a/xray/commands/audit/jasrunner_test.go b/xray/commands/audit/jasrunner_test.go new file mode 100644 index 000000000..b6bd121df --- /dev/null +++ b/xray/commands/audit/jasrunner_test.go @@ -0,0 +1,44 @@ +package audit + +import ( + rtutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/jas" + "github.com/jfrog/jfrog-cli-core/v2/xray/utils" + "github.com/jfrog/jfrog-client-go/utils/io/fileutils" + "github.com/stretchr/testify/assert" + "os" + "testing" +) + +func TestGetExtendedScanResults_AnalyzerManagerDoesntExist(t *testing.T) { + tmpDir, err := fileutils.CreateTempDir() + defer func() { + assert.NoError(t, fileutils.RemoveTempDir(tmpDir)) + }() + assert.NoError(t, err) + assert.NoError(t, os.Setenv(coreutils.HomeDir, tmpDir)) + defer func() { + assert.NoError(t, os.Unsetenv(coreutils.HomeDir)) + }() + scanResults := &utils.ExtendedScanResults{XrayResults: jas.FakeBasicXrayResults, ScannedTechnologies: []coreutils.Technology{coreutils.Yarn}} + err = runJasScannersAndSetResults(scanResults, []string{"issueId_1_direct_dependency", "issueId_2_direct_dependency"}, &jas.FakeServerDetails, nil, nil, "") + // Expect error: + assert.Error(t, err) +} + +func TestGetExtendedScanResults_ServerNotValid(t *testing.T) { + scanResults := &utils.ExtendedScanResults{XrayResults: jas.FakeBasicXrayResults, ScannedTechnologies: []coreutils.Technology{coreutils.Pip}} + err := runJasScannersAndSetResults(scanResults, []string{"issueId_1_direct_dependency", "issueId_2_direct_dependency"}, nil, nil, nil, "") + assert.NoError(t, err) +} + +func TestGetExtendedScanResults_AnalyzerManagerReturnsError(t *testing.T) { + mockDirectDependencies := []string{"issueId_2_direct_dependency", "issueId_1_direct_dependency"} + assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) + scanResults := &utils.ExtendedScanResults{XrayResults: jas.FakeBasicXrayResults, ScannedTechnologies: []coreutils.Technology{coreutils.Yarn}} + err := runJasScannersAndSetResults(scanResults, mockDirectDependencies, &jas.FakeServerDetails, nil, nil, "") + + // Expect error: + assert.ErrorContains(t, err, "failed to run Applicability scan") +} diff --git a/xray/audit/commonutils.go b/xray/commands/audit/sca/common.go similarity index 66% rename from xray/audit/commonutils.go rename to xray/commands/audit/sca/common.go index b2f79997b..094746024 100644 --- a/xray/audit/commonutils.go +++ b/xray/commands/audit/sca/common.go @@ -1,90 +1,85 @@ -package audit +package sca import ( "fmt" + biutils "github.com/jfrog/build-info-go/utils" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - xraycommands "github.com/jfrog/jfrog-cli-core/v2/xray/commands/utils" - xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" - "os" - "os/exec" - "path/filepath" - "strings" - "testing" - - buildinfo "github.com/jfrog/build-info-go/entities" "github.com/jfrog/jfrog-cli-core/v2/utils/tests" + "github.com/jfrog/jfrog-cli-core/v2/xray/scangraph" "github.com/jfrog/jfrog-client-go/utils/errorutils" ioUtils "github.com/jfrog/jfrog-client-go/utils/io" - "github.com/jfrog/jfrog-client-go/utils/io/fileutils" "github.com/jfrog/jfrog-client-go/utils/log" testsutils "github.com/jfrog/jfrog-client-go/utils/tests" "github.com/jfrog/jfrog-client-go/xray/services" + xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" "github.com/stretchr/testify/assert" - "golang.org/x/exp/slices" + "golang.org/x/exp/maps" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" ) -func BuildXrayDependencyTree(treeHelper map[string][]string, nodeId string) *xrayUtils.GraphNode { - exceededDepthCounter := 0 - xrayDependencyTree := buildXrayDependencyTree(treeHelper, []string{nodeId}, &exceededDepthCounter) - if exceededDepthCounter > 0 { - log.Debug("buildXrayDependencyTree exceeded max tree depth", exceededDepthCounter, "times") +const maxUniqueAppearances = 10 + +func BuildXrayDependencyTree(treeHelper map[string][]string, nodeId string) (*xrayUtils.GraphNode, []string) { + rootNode := &xrayUtils.GraphNode{ + Id: nodeId, + Nodes: []*xrayUtils.GraphNode{}, } - return xrayDependencyTree + dependencyAppearances := map[string]int8{} + populateXrayDependencyTree(rootNode, treeHelper, &dependencyAppearances) + return rootNode, maps.Keys(dependencyAppearances) } -func buildXrayDependencyTree(treeHelper map[string][]string, impactPath []string, exceededDepthCounter *int) *xrayUtils.GraphNode { - nodeId := impactPath[len(impactPath)-1] - // Initialize the new node - xrDependencyTree := &xrayUtils.GraphNode{} - xrDependencyTree.Id = nodeId - xrDependencyTree.Nodes = []*xrayUtils.GraphNode{} - if len(impactPath) >= buildinfo.RequestedByMaxLength { - *exceededDepthCounter++ - return xrDependencyTree - } +func populateXrayDependencyTree(currNode *xrayUtils.GraphNode, treeHelper map[string][]string, dependencyAppearances *map[string]int8) { + (*dependencyAppearances)[currNode.Id]++ // Recursively create & append all node's dependencies. - for _, dependency := range treeHelper[nodeId] { - // Prevent circular dependencies parsing - if slices.Contains(impactPath, dependency) { + for _, childDepId := range treeHelper[currNode.Id] { + childNode := &xrayUtils.GraphNode{ + Id: childDepId, + Nodes: []*xrayUtils.GraphNode{}, + Parent: currNode, + } + if (*dependencyAppearances)[childDepId] >= maxUniqueAppearances || childNode.NodeHasLoop() { continue } - xrDependencyTree.Nodes = append(xrDependencyTree.Nodes, buildXrayDependencyTree(treeHelper, append(impactPath, dependency), exceededDepthCounter)) + currNode.Nodes = append(currNode.Nodes, childNode) + populateXrayDependencyTree(childNode, treeHelper, dependencyAppearances) } - return xrDependencyTree } -func RunXrayDependenciesTreeScanGraph(modulesDependencyTrees []*xrayUtils.GraphNode, progress ioUtils.ProgressMgr, technology coreutils.Technology, scanGraphParams *xraycommands.ScanGraphParams) (results []services.ScanResponse, err error) { +func RunXrayDependenciesTreeScanGraph(dependencyTree *xrayUtils.GraphNode, progress ioUtils.ProgressMgr, technology coreutils.Technology, scanGraphParams *scangraph.ScanGraphParams) (results []services.ScanResponse, err error) { + scanGraphParams.XrayGraphScanParams().DependenciesGraph = dependencyTree + xscGitInfoContext := scanGraphParams.XrayGraphScanParams().XscGitInfoContext + if xscGitInfoContext != nil { + xscGitInfoContext.Technologies = []string{technology.ToString()} + } + scanMessage := fmt.Sprintf("Scanning %d %s dependencies", len(dependencyTree.Nodes), technology) if progress != nil { - progress.SetHeadlineMsg("Scanning for vulnerabilities") + progress.SetHeadlineMsg(scanMessage) } - - for _, moduleDependencyTree := range modulesDependencyTrees { - scanGraphParams.XrayGraphScanParams().Graph = moduleDependencyTree - scanMessage := fmt.Sprintf("Scanning %d %s dependencies", len(scanGraphParams.XrayGraphScanParams().Graph.Nodes), technology) - if progress != nil { - progress.SetHeadlineMsg(scanMessage) - } - log.Info(scanMessage + "...") - var scanResults *services.ScanResponse - scanResults, err = xraycommands.RunScanGraphAndGetResults(scanGraphParams) - if err != nil { - err = errorutils.CheckErrorf("scanning %s dependencies failed with error: %s", string(technology), err.Error()) - return - } - for i := range scanResults.Vulnerabilities { - scanResults.Vulnerabilities[i].Technology = technology.ToString() - } - for i := range scanResults.Violations { - scanResults.Violations[i].Technology = technology.ToString() - } - results = append(results, *scanResults) + log.Info(scanMessage + "...") + var scanResults *services.ScanResponse + scanResults, err = scangraph.RunScanGraphAndGetResults(scanGraphParams) + if err != nil { + err = errorutils.CheckErrorf("scanning %s dependencies failed with error: %s", string(technology), err.Error()) + return + } + for i := range scanResults.Vulnerabilities { + scanResults.Vulnerabilities[i].Technology = technology.ToString() } + for i := range scanResults.Violations { + scanResults.Violations[i].Technology = technology.ToString() + } + results = append(results, *scanResults) return } func CreateTestWorkspace(t *testing.T, sourceDir string) (string, func()) { tempDirPath, createTempDirCallback := tests.CreateTempDirWithCallbackAndAssert(t) - assert.NoError(t, fileutils.CopyDir(filepath.Join("..", "..", "commands", "testdata", sourceDir), tempDirPath, true, nil)) + assert.NoError(t, biutils.CopyDir(filepath.Join("..", "..", "..", "testdata", sourceDir), tempDirPath, true, nil)) wd, err := os.Getwd() assert.NoError(t, err, "Failed to get current dir") chdirCallback := testsutils.ChangeDirWithCallback(t, wd, tempDirPath) @@ -117,14 +112,18 @@ func GetModule(modules []*xrayUtils.GraphNode, moduleId string) *xrayUtils.Graph // GetExecutableVersion gets an executable version and prints to the debug log if possible. // Only supported for package managers that use "--version". -func GetExecutableVersion(executable string) (version string, err error) { +func LogExecutableVersion(executable string) { verBytes, err := exec.Command(executable, "--version").CombinedOutput() - if err != nil || len(verBytes) == 0 { - return "", err + if err != nil { + log.Debug(fmt.Sprintf("'%q --version' command received an error: %s", executable, err.Error())) + return + } + if len(verBytes) == 0 { + log.Debug(fmt.Sprintf("'%q --version' command received an empty response", executable)) + return } - version = strings.TrimSpace(string(verBytes)) + version := strings.TrimSpace(string(verBytes)) log.Debug(fmt.Sprintf("Used %q version: %s", executable, version)) - return } // BuildImpactPathsForScanResponse builds the full impact paths for each vulnerability found in the scanResult argument, using the dependencyTrees argument. diff --git a/xray/audit/commonutils_test.go b/xray/commands/audit/sca/common_test.go similarity index 99% rename from xray/audit/commonutils_test.go rename to xray/commands/audit/sca/common_test.go index db4e2158c..a43965b64 100644 --- a/xray/audit/commonutils_test.go +++ b/xray/commands/audit/sca/common_test.go @@ -1,4 +1,4 @@ -package audit +package sca import ( "github.com/jfrog/jfrog-client-go/xray/services" diff --git a/xray/audit/go/gloang_test.go b/xray/commands/audit/sca/go/gloang_test.go similarity index 60% rename from xray/audit/go/gloang_test.go rename to xray/commands/audit/sca/go/gloang_test.go index 82e0aced8..7a8722efe 100644 --- a/xray/audit/go/gloang_test.go +++ b/xray/commands/audit/sca/go/gloang_test.go @@ -3,11 +3,11 @@ package _go import ( "github.com/jfrog/build-info-go/utils" "github.com/jfrog/jfrog-cli-core/v2/utils/config" + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/sca" "os" "strings" "testing" - "github.com/jfrog/jfrog-cli-core/v2/xray/audit" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" "github.com/stretchr/testify/assert" @@ -15,7 +15,7 @@ import ( func TestBuildGoDependencyList(t *testing.T) { // Create and change directory to test workspace - _, cleanUp := audit.CreateTestWorkspace(t, "go-project") + _, cleanUp := sca.CreateTestWorkspace(t, "go-project") defer cleanUp() err := removeTxtSuffix("go.mod.txt") @@ -32,8 +32,19 @@ func TestBuildGoDependencyList(t *testing.T) { User: "user", AccessToken: "sdsdccs2232", } - rootNode, err := BuildDependencyTree(server, "test-remote") + goVersionID, err := getGoVersionAsDependency() assert.NoError(t, err) + expectedUniqueDeps := []string{ + goPackageTypeIdentifier + "golang.org/x/text:v0.3.3", + goPackageTypeIdentifier + "rsc.io/quote:v1.5.2", + goPackageTypeIdentifier + "rsc.io/sampler:v1.3.0", + goPackageTypeIdentifier + "testGoList", + goVersionID.Id, + } + rootNode, uniqueDeps, err := BuildDependencyTree(server, "test-remote") + assert.NoError(t, err) + assert.ElementsMatch(t, uniqueDeps, expectedUniqueDeps, "First is actual, Second is Expected") + assert.Equal(t, "https://user:sdsdccs2232@api.go.here/artifactoryapi/go/test-remote|direct", os.Getenv("GOPROXY")) assert.NotEmpty(t, rootNode) @@ -44,16 +55,16 @@ func TestBuildGoDependencyList(t *testing.T) { // Test go version node goVersion, err := utils.GetParsedGoVersion() assert.NoError(t, err) - audit.GetAndAssertNode(t, rootNode[0].Nodes, strings.ReplaceAll(goVersion.GetVersion(), "go", goSourceCodePrefix)) + sca.GetAndAssertNode(t, rootNode[0].Nodes, strings.ReplaceAll(goVersion.GetVersion(), "go", goSourceCodePrefix)) // Test child without sub nodes - child1 := audit.GetAndAssertNode(t, rootNode[0].Nodes, "golang.org/x/text:v0.3.3") + child1 := sca.GetAndAssertNode(t, rootNode[0].Nodes, "golang.org/x/text:v0.3.3") assert.Len(t, child1.Nodes, 0) // Test child with 1 sub node - child2 := audit.GetAndAssertNode(t, rootNode[0].Nodes, "rsc.io/quote:v1.5.2") + child2 := sca.GetAndAssertNode(t, rootNode[0].Nodes, "rsc.io/quote:v1.5.2") assert.Len(t, child2.Nodes, 1) - audit.GetAndAssertNode(t, child2.Nodes, "rsc.io/sampler:v1.3.0") + sca.GetAndAssertNode(t, child2.Nodes, "rsc.io/sampler:v1.3.0") } func removeTxtSuffix(txtFileName string) error { diff --git a/xray/audit/go/golang.go b/xray/commands/audit/sca/go/golang.go similarity index 79% rename from xray/audit/go/golang.go rename to xray/commands/audit/sca/go/golang.go index f6d0010fa..86c35fc12 100644 --- a/xray/audit/go/golang.go +++ b/xray/commands/audit/sca/go/golang.go @@ -2,6 +2,7 @@ package _go import ( "github.com/jfrog/build-info-go/utils" + "github.com/jfrog/gofrog/datastructures" "github.com/jfrog/jfrog-cli-core/v2/utils/config" "os" "strings" @@ -16,7 +17,7 @@ const ( goSourceCodePrefix = "github.com/golang/go:v" ) -func BuildDependencyTree(server *config.ServerDetails, remoteGoRepo string) (dependencyTree []*xrayUtils.GraphNode, err error) { +func BuildDependencyTree(server *config.ServerDetails, remoteGoRepo string) (dependencyTree []*xrayUtils.GraphNode, uniqueDeps []string, err error) { currentDir, err := coreutils.GetWorkingDirectory() if err != nil { return @@ -46,15 +47,18 @@ func BuildDependencyTree(server *config.ServerDetails, remoteGoRepo string) (dep Id: goPackageTypeIdentifier + rootModuleName, Nodes: []*xrayUtils.GraphNode{}, } - populateGoDependencyTree(rootNode, dependenciesGraph, dependenciesList) + uniqueDepsSet := datastructures.MakeSet[string]() + populateGoDependencyTree(rootNode, dependenciesGraph, dependenciesList, uniqueDepsSet) - // Add go version as child node to dependencies tree - err = addGoVersionAsDependency(rootNode) + goVersionDependency, err := getGoVersionAsDependency() if err != nil { return } + rootNode.Nodes = append(rootNode.Nodes, goVersionDependency) + uniqueDepsSet.Add(goVersionDependency.Id) dependencyTree = []*xrayUtils.GraphNode{rootNode} + uniqueDeps = uniqueDepsSet.ToSlice() return } @@ -67,10 +71,11 @@ func setGoProxy(server *config.ServerDetails, remoteGoRepo string) error { return os.Setenv("GOPROXY", repoUrl) } -func populateGoDependencyTree(currNode *xrayUtils.GraphNode, dependenciesGraph map[string][]string, dependenciesList map[string]bool) { +func populateGoDependencyTree(currNode *xrayUtils.GraphNode, dependenciesGraph map[string][]string, dependenciesList map[string]bool, uniqueDepsSet *datastructures.Set[string]) { if currNode.NodeHasLoop() { return } + uniqueDepsSet.Add(currNode.Id) currDepChildren := dependenciesGraph[strings.TrimPrefix(currNode.Id, goPackageTypeIdentifier)] // Recursively create & append all node's dependencies. for _, childName := range currDepChildren { @@ -84,20 +89,18 @@ func populateGoDependencyTree(currNode *xrayUtils.GraphNode, dependenciesGraph m Parent: currNode, } currNode.Nodes = append(currNode.Nodes, childNode) - populateGoDependencyTree(childNode, dependenciesGraph, dependenciesList) + populateGoDependencyTree(childNode, dependenciesGraph, dependenciesList, uniqueDepsSet) } } -func addGoVersionAsDependency(rootNode *xrayUtils.GraphNode) error { +func getGoVersionAsDependency() (*xrayUtils.GraphNode, error) { goVersion, err := utils.GetParsedGoVersion() if err != nil { - return err + return nil, err } // Convert "go1.17.3" to "github.com/golang/go:v1.17.3" goVersionID := strings.ReplaceAll(goVersion.GetVersion(), "go", goSourceCodePrefix) - rootNode.Nodes = append(rootNode.Nodes, &xrayUtils.GraphNode{ - Id: goPackageTypeIdentifier + goVersionID, - Nodes: []*xrayUtils.GraphNode{}, - }) - return nil + return &xrayUtils.GraphNode{ + Id: goPackageTypeIdentifier + goVersionID, + }, nil } diff --git a/xray/audit/java/gradle-dep-tree.jar b/xray/commands/audit/sca/java/gradle-dep-tree.jar similarity index 100% rename from xray/audit/java/gradle-dep-tree.jar rename to xray/commands/audit/sca/java/gradle-dep-tree.jar diff --git a/xray/audit/java/gradle.go b/xray/commands/audit/sca/java/gradle.go similarity index 93% rename from xray/audit/java/gradle.go rename to xray/commands/audit/sca/java/gradle.go index 5db177045..d140efffd 100644 --- a/xray/audit/java/gradle.go +++ b/xray/commands/audit/sca/java/gradle.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/jfrog/gofrog/datastructures" "os" "os/exec" "path/filepath" @@ -105,11 +106,13 @@ func (dtp *depTreeManager) appendDependenciesPaths(jsonDepTree []byte, fileName if dtp.tree == nil { dtp.tree = make(map[string][]dependenciesPaths) } - dtp.tree[fileName] = append(dtp.tree[fileName], deps) + if len(deps.Paths) > 0 { + dtp.tree[fileName] = append(dtp.tree[fileName], deps) + } return nil } -func buildGradleDependencyTree(params *DependencyTreeParams) (dependencyTree []*xrayUtils.GraphNode, err error) { +func buildGradleDependencyTree(params *DependencyTreeParams) (dependencyTree []*xrayUtils.GraphNode, uniqueDeps []string, err error) { manager := &depTreeManager{useWrapper: params.UseWrapper} if params.IgnoreConfigFile { // In case we don't need to use the gradle config file, @@ -125,9 +128,10 @@ func buildGradleDependencyTree(params *DependencyTreeParams) (dependencyTree []* outputFileContent, err := manager.runGradleDepTree() if err != nil { - return nil, err + return } - return manager.getGraphFromDepTree(outputFileContent) + dependencyTree, uniqueDeps, err = manager.getGraphFromDepTree(outputFileContent) + return } func (dtp *depTreeManager) runGradleDepTree() (outputFileContent []byte, err error) { @@ -234,25 +238,27 @@ func (dtp *depTreeManager) execGradleDepTree(depTreeDir string) (outputFileConte } // Assuming we ran gradle-dep-tree, getGraphFromDepTree receives the content of the depTreeOutputFile as input -func (dtp *depTreeManager) getGraphFromDepTree(outputFileContent []byte) ([]*xrayUtils.GraphNode, error) { +func (dtp *depTreeManager) getGraphFromDepTree(outputFileContent []byte) ([]*xrayUtils.GraphNode, []string, error) { if err := dtp.parseDepTreeFiles(outputFileContent); err != nil { - return nil, err + return nil, nil, err } var depsGraph []*xrayUtils.GraphNode + uniqueDepsSet := datastructures.MakeSet[string]() for dependency, children := range dtp.tree { directDependency := &xrayUtils.GraphNode{ Id: GavPackageTypeIdentifier + dependency, Nodes: []*xrayUtils.GraphNode{}, } for _, childPath := range children { - populateGradleDependencyTree(directDependency, childPath) + populateGradleDependencyTree(directDependency, childPath, uniqueDepsSet) } depsGraph = append(depsGraph, directDependency) } - return depsGraph, nil + return depsGraph, uniqueDepsSet.ToSlice(), nil } -func populateGradleDependencyTree(currNode *xrayUtils.GraphNode, currNodeChildren dependenciesPaths) { +func populateGradleDependencyTree(currNode *xrayUtils.GraphNode, currNodeChildren dependenciesPaths, uniqueDepsSet *datastructures.Set[string]) { + uniqueDepsSet.Add(currNode.Id) for gav, children := range currNodeChildren.Paths { childNode := &xrayUtils.GraphNode{ Id: GavPackageTypeIdentifier + gav, @@ -262,7 +268,7 @@ func populateGradleDependencyTree(currNode *xrayUtils.GraphNode, currNodeChildre if currNode.NodeHasLoop() { return } - populateGradleDependencyTree(childNode, children) + populateGradleDependencyTree(childNode, children, uniqueDepsSet) currNode.Nodes = append(currNode.Nodes, childNode) } } diff --git a/xray/audit/java/gradle_test.go b/xray/commands/audit/sca/java/gradle_test.go similarity index 75% rename from xray/audit/java/gradle_test.go rename to xray/commands/audit/sca/java/gradle_test.go index 591e7ef88..e9d09b742 100644 --- a/xray/audit/java/gradle_test.go +++ b/xray/commands/audit/sca/java/gradle_test.go @@ -3,6 +3,7 @@ package java import ( "errors" "fmt" + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/sca" "os" "path/filepath" "testing" @@ -12,8 +13,6 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-core/v2/utils/ioutils" - "github.com/jfrog/jfrog-cli-core/v2/xray/audit" - "github.com/stretchr/testify/assert" ) @@ -41,65 +40,65 @@ allprojects { func TestGradleTreesWithoutConfig(t *testing.T) { // Create and change directory to test workspace - tempDirPath, cleanUp := audit.CreateTestWorkspace(t, "gradle-example-ci-server") + tempDirPath, cleanUp := sca.CreateTestWorkspace(t, "gradle-example-ci-server") defer cleanUp() assert.NoError(t, os.Chmod(filepath.Join(tempDirPath, "gradlew"), 0700)) // Run getModulesDependencyTrees - modulesDependencyTrees, err := buildGradleDependencyTree(&DependencyTreeParams{}) + modulesDependencyTrees, uniqueDeps, err := buildGradleDependencyTree(&DependencyTreeParams{}) if assert.NoError(t, err) && assert.NotNil(t, modulesDependencyTrees) { - assert.Len(t, modulesDependencyTrees, 5) + assert.Len(t, uniqueDeps, 11) + assert.Len(t, modulesDependencyTrees, 2) // Check module - module := audit.GetAndAssertNode(t, modulesDependencyTrees, "webservice") + module := sca.GetAndAssertNode(t, modulesDependencyTrees, "webservice") assert.Len(t, module.Nodes, 7) // Check direct dependency - directDependency := audit.GetAndAssertNode(t, module.Nodes, "junit:junit:4.11") + directDependency := sca.GetAndAssertNode(t, module.Nodes, "junit:junit:4.11") assert.Len(t, directDependency.Nodes, 1) // Check transitive dependency - audit.GetAndAssertNode(t, directDependency.Nodes, "org.hamcrest:hamcrest-core:1.3") + sca.GetAndAssertNode(t, directDependency.Nodes, "org.hamcrest:hamcrest-core:1.3") } } func TestGradleTreesWithConfig(t *testing.T) { // Create and change directory to test workspace - tempDirPath, cleanUp := audit.CreateTestWorkspace(t, "gradle-example-publish") + tempDirPath, cleanUp := sca.CreateTestWorkspace(t, "gradle-example-publish") defer cleanUp() assert.NoError(t, os.Chmod(filepath.Join(tempDirPath, "gradlew"), 0700)) // Run getModulesDependencyTrees - modulesDependencyTrees, err := buildGradleDependencyTree(&DependencyTreeParams{UseWrapper: true}) + modulesDependencyTrees, uniqueDeps, err := buildGradleDependencyTree(&DependencyTreeParams{UseWrapper: true}) if assert.NoError(t, err) && assert.NotNil(t, modulesDependencyTrees) { - assert.Len(t, modulesDependencyTrees, 5) - + assert.Len(t, modulesDependencyTrees, 3) + assert.Len(t, uniqueDeps, 11) // Check module - module := audit.GetAndAssertNode(t, modulesDependencyTrees, "api") + module := sca.GetAndAssertNode(t, modulesDependencyTrees, "api") assert.Len(t, module.Nodes, 4) // Check direct dependency - directDependency := audit.GetAndAssertNode(t, module.Nodes, "commons-lang:commons-lang:2.4") + directDependency := sca.GetAndAssertNode(t, module.Nodes, "commons-lang:commons-lang:2.4") assert.Len(t, directDependency.Nodes, 1) // Check transitive dependency - audit.GetAndAssertNode(t, directDependency.Nodes, "commons-io:commons-io:1.2") + sca.GetAndAssertNode(t, directDependency.Nodes, "commons-io:commons-io:1.2") } } func TestGradleTreesExcludeTestDeps(t *testing.T) { // Create and change directory to test workspace - tempDirPath, cleanUp := audit.CreateTestWorkspace(t, "gradle-example-ci-server") + tempDirPath, cleanUp := sca.CreateTestWorkspace(t, "gradle-example-ci-server") defer cleanUp() assert.NoError(t, os.Chmod(filepath.Join(tempDirPath, "gradlew"), 0700)) // Run getModulesDependencyTrees - modulesDependencyTrees, err := buildGradleDependencyTree(&DependencyTreeParams{UseWrapper: true}) + modulesDependencyTrees, uniqueDeps, err := buildGradleDependencyTree(&DependencyTreeParams{UseWrapper: true}) if assert.NoError(t, err) && assert.NotNil(t, modulesDependencyTrees) { - assert.Len(t, modulesDependencyTrees, 5) - + assert.Len(t, modulesDependencyTrees, 2) + assert.Len(t, uniqueDeps, 11) // Check direct dependency - directDependency := audit.GetAndAssertNode(t, modulesDependencyTrees, "services") - assert.Empty(t, directDependency.Nodes) + assert.Nil(t, sca.GetModule(modulesDependencyTrees, "services")) } } @@ -110,7 +109,7 @@ func TestIsGradleWrapperExist(t *testing.T) { assert.NoError(t, err) // Check Gradle wrapper exist - _, cleanUp := audit.CreateTestWorkspace(t, "gradle-example-ci-server") + _, cleanUp := sca.CreateTestWorkspace(t, "gradle-example-ci-server") defer cleanUp() isWrapperExist, err = isGradleWrapperExist() assert.NoError(t, err) @@ -171,17 +170,18 @@ func TestGetDepTreeArtifactoryRepository(t *testing.T) { func TestGetGraphFromDepTree(t *testing.T) { // Create and change directory to test workspace - tempDirPath, cleanUp := audit.CreateTestWorkspace(t, "gradle-example-ci-server") + tempDirPath, cleanUp := sca.CreateTestWorkspace(t, "gradle-example-ci-server") defer func() { cleanUp() }() assert.NoError(t, os.Chmod(filepath.Join(tempDirPath, "gradlew"), 0700)) testCase := struct { - name string - expectedResult map[string]map[string]string + name string + expectedTree map[string]map[string]string + expectedUniqueDeps []string }{ name: "ValidOutputFileContent", - expectedResult: map[string]map[string]string{ + expectedTree: map[string]map[string]string{ GavPackageTypeIdentifier + "shared": {}, GavPackageTypeIdentifier + filepath.Base(tempDirPath): {}, GavPackageTypeIdentifier + "services": {}, @@ -200,15 +200,30 @@ func TestGetGraphFromDepTree(t *testing.T) { GavPackageTypeIdentifier + "commons-lang:commons-lang:2.4": "", }, }, + expectedUniqueDeps: []string{ + GavPackageTypeIdentifier + "webservice", + GavPackageTypeIdentifier + "junit:junit:4.11", + GavPackageTypeIdentifier + "commons-io:commons-io:1.2", + GavPackageTypeIdentifier + "org.apache.wicket:wicket:1.3.7", + GavPackageTypeIdentifier + "org.jfrog.example.gradle:shared:1.0", + GavPackageTypeIdentifier + "org.jfrog.example.gradle:api:1.0", + GavPackageTypeIdentifier + "commons-collections:commons-collections:3.2", + GavPackageTypeIdentifier + "api", + GavPackageTypeIdentifier + "commons-lang:commons-lang:2.4", + GavPackageTypeIdentifier + "org.hamcrest:hamcrest-core:1.3", + GavPackageTypeIdentifier + "org.slf4j:slf4j-api:1.4.2", + }, } manager := &depTreeManager{} outputFileContent, err := manager.runGradleDepTree() assert.NoError(t, err) - result, err := (&depTreeManager{}).getGraphFromDepTree(outputFileContent) + depTree, uniqueDeps, err := (&depTreeManager{}).getGraphFromDepTree(outputFileContent) assert.NoError(t, err) - for _, dependency := range result { - depChild, exists := testCase.expectedResult[dependency.Id] + assert.ElementsMatch(t, uniqueDeps, testCase.expectedUniqueDeps, "First is actual, Second is Expected") + + for _, dependency := range depTree { + depChild, exists := testCase.expectedTree[dependency.Id] assert.True(t, exists) assert.Equal(t, len(depChild), len(dependency.Nodes)) } diff --git a/xray/audit/java/javautils.go b/xray/commands/audit/sca/java/javautils.go similarity index 73% rename from xray/audit/java/javautils.go rename to xray/commands/audit/sca/java/javautils.go index 93421aaf1..9653de43f 100644 --- a/xray/audit/java/javautils.go +++ b/xray/commands/audit/sca/java/javautils.go @@ -1,8 +1,10 @@ package java import ( + "github.com/jfrog/gofrog/datastructures" "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + xrayutils "github.com/jfrog/jfrog-cli-core/v2/xray/utils" xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" "strconv" "time" @@ -40,35 +42,36 @@ func createBuildConfiguration(buildName string) (*artifactoryUtils.BuildConfigur // Create a dependency tree for each one of the modules in the build. // buildName - audit-mvn or audit-gradle -func createGavDependencyTree(buildConfig *artifactoryUtils.BuildConfiguration) ([]*xrayUtils.GraphNode, error) { +func createGavDependencyTree(buildConfig *artifactoryUtils.BuildConfiguration) ([]*xrayUtils.GraphNode, []string, error) { buildName, err := buildConfig.GetBuildName() if err != nil { - return nil, err + return nil, nil, err } buildNumber, err := buildConfig.GetBuildNumber() if err != nil { - return nil, err + return nil, nil, err } generatedBuildsInfos, err := artifactoryUtils.GetGeneratedBuildsInfo(buildName, buildNumber, buildConfig.GetProject()) if err != nil { - return nil, err + return nil, nil, err } if len(generatedBuildsInfos) == 0 { - return nil, errorutils.CheckErrorf("Couldn't find build " + buildName + "/" + buildNumber) + return nil, nil, errorutils.CheckErrorf("Couldn't find build " + buildName + "/" + buildNumber) } modules := []*xrayUtils.GraphNode{} + uniqueDepsSet := datastructures.MakeSet[string]() for _, module := range generatedBuildsInfos[0].Modules { - modules = append(modules, addModuleTree(module)) + modules = append(modules, addModuleTree(module, uniqueDepsSet)) } - return modules, nil + return modules, uniqueDepsSet.ToSlice(), nil } -func addModuleTree(module buildinfo.Module) *xrayUtils.GraphNode { +func addModuleTree(module buildinfo.Module, uniqueDepsSet *datastructures.Set[string]) *xrayUtils.GraphNode { moduleTree := &xrayUtils.GraphNode{ Id: GavPackageTypeIdentifier + module.Id, } - + uniqueDepsSet.Add(moduleTree.Id) directDependencies := make(map[string]buildinfo.Dependency) parentToChildren := newDependencyMultimap() for index, dependency := range module.Dependencies { @@ -86,7 +89,7 @@ func addModuleTree(module buildinfo.Module) *xrayUtils.GraphNode { } for _, directDependency := range directDependencies { - populateTransitiveDependencies(moduleTree, directDependency.Id, parentToChildren, []string{}) + populateTransitiveDependencies(moduleTree, directDependency.Id, parentToChildren, []string{}, uniqueDepsSet) } return moduleTree } @@ -105,7 +108,7 @@ func isDirectDependency(moduleId string, requestedBy [][]string) bool { return false } -func populateTransitiveDependencies(parent *xrayUtils.GraphNode, dependencyId string, parentToChildren *dependencyMultimap, idsAdded []string) { +func populateTransitiveDependencies(parent *xrayUtils.GraphNode, dependencyId string, parentToChildren *dependencyMultimap, idsAdded []string, uniqueDepsSet *datastructures.Set[string]) { if hasLoop(idsAdded, dependencyId) { return } @@ -114,9 +117,10 @@ func populateTransitiveDependencies(parent *xrayUtils.GraphNode, dependencyId st Id: GavPackageTypeIdentifier + dependencyId, Nodes: []*xrayUtils.GraphNode{}, } + uniqueDepsSet.Add(node.Id) parent.Nodes = append(parent.Nodes, node) for _, child := range parentToChildren.getChildren(node.Id) { - populateTransitiveDependencies(node, child.Id, parentToChildren, idsAdded) + populateTransitiveDependencies(node, child.Id, parentToChildren, idsAdded, uniqueDepsSet) } } @@ -129,11 +133,24 @@ func hasLoop(idsAdded []string, idToAdd string) bool { return false } -func BuildDependencyTree(params *DependencyTreeParams) (modules []*xrayUtils.GraphNode, err error) { - if params.Tool == coreutils.Maven { - return buildMvnDependencyTree(params) +func BuildDependencyTree(params *xrayutils.AuditBasicParams, tech coreutils.Technology) ([]*xrayUtils.GraphNode, []string, error) { + serverDetails, err := params.ServerDetails() + if err != nil { + return nil, nil, err + } + dependencyTreeParams := &DependencyTreeParams{ + Tool: tech, + InsecureTls: params.InsecureTls(), + IgnoreConfigFile: params.IgnoreConfigFile(), + ExcludeTestDeps: params.ExcludeTestDependencies(), + UseWrapper: params.UseWrapper(), + Server: serverDetails, + DepsRepo: params.DepsRepo(), + } + if tech == coreutils.Maven { + return buildMvnDependencyTree(dependencyTreeParams) } - return buildGradleDependencyTree(params) + return buildGradleDependencyTree(dependencyTreeParams) } type dependencyMultimap struct { diff --git a/xray/audit/java/mvn.go b/xray/commands/audit/sca/java/mvn.go similarity index 97% rename from xray/audit/java/mvn.go rename to xray/commands/audit/sca/java/mvn.go index f4039d585..554457377 100644 --- a/xray/audit/java/mvn.go +++ b/xray/commands/audit/sca/java/mvn.go @@ -13,7 +13,7 @@ import ( xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" ) -func buildMvnDependencyTree(params *DependencyTreeParams) (modules []*xrayUtils.GraphNode, err error) { +func buildMvnDependencyTree(params *DependencyTreeParams) (modules []*xrayUtils.GraphNode, uniqueDeps []string, err error) { buildConfiguration, cleanBuild := createBuildConfiguration("audit-mvn") defer func() { err = errors.Join(err, cleanBuild()) diff --git a/xray/commands/audit/sca/java/mvn_test.go b/xray/commands/audit/sca/java/mvn_test.go new file mode 100644 index 000000000..bf4f8d9ce --- /dev/null +++ b/xray/commands/audit/sca/java/mvn_test.go @@ -0,0 +1,100 @@ +package java + +import ( + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/sca" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMavenTreesMultiModule(t *testing.T) { + // Create and change directory to test workspace + _, cleanUp := sca.CreateTestWorkspace(t, "maven-example") + defer cleanUp() + + expectedUniqueDeps := []string{ + GavPackageTypeIdentifier + "javax.mail:mail:1.4", + GavPackageTypeIdentifier + "org.testng:testng:5.9", + GavPackageTypeIdentifier + "javax.servlet:servlet-api:2.5", + GavPackageTypeIdentifier + "org.jfrog.test:multi:3.7-SNAPSHOT", + GavPackageTypeIdentifier + "org.jfrog.test:multi3:3.7-SNAPSHOT", + GavPackageTypeIdentifier + "org.jfrog.test:multi2:3.7-SNAPSHOT", + GavPackageTypeIdentifier + "junit:junit:3.8.1", + GavPackageTypeIdentifier + "org.jfrog.test:multi1:3.7-SNAPSHOT", + GavPackageTypeIdentifier + "commons-io:commons-io:1.4", + GavPackageTypeIdentifier + "org.apache.commons:commons-email:1.1", + GavPackageTypeIdentifier + "javax.activation:activation:1.1", + GavPackageTypeIdentifier + "hsqldb:hsqldb:1.8.0.10", + } + // Run getModulesDependencyTrees + modulesDependencyTrees, uniqueDeps, err := buildMvnDependencyTree(&DependencyTreeParams{IgnoreConfigFile: true}) + if assert.NoError(t, err) && assert.NotEmpty(t, modulesDependencyTrees) { + assert.ElementsMatch(t, uniqueDeps, expectedUniqueDeps, "First is actual, Second is Expected") + // Check root module + multi := sca.GetAndAssertNode(t, modulesDependencyTrees, "org.jfrog.test:multi:3.7-SNAPSHOT") + if assert.NotNil(t, multi) { + assert.Empty(t, multi.Nodes) + // Check multi1 with a transitive dependency + multi1 := sca.GetAndAssertNode(t, modulesDependencyTrees, "org.jfrog.test:multi1:3.7-SNAPSHOT") + assert.Len(t, multi1.Nodes, 4) + commonsEmail := sca.GetAndAssertNode(t, multi1.Nodes, "org.apache.commons:commons-email:1.1") + assert.Len(t, commonsEmail.Nodes, 2) + + // Check multi2 and multi3 + multi2 := sca.GetAndAssertNode(t, modulesDependencyTrees, "org.jfrog.test:multi2:3.7-SNAPSHOT") + assert.Len(t, multi2.Nodes, 1) + multi3 := sca.GetAndAssertNode(t, modulesDependencyTrees, "org.jfrog.test:multi3:3.7-SNAPSHOT") + assert.Len(t, multi3.Nodes, 4) + } + } +} + +func TestMavenWrapperTrees(t *testing.T) { + // Create and change directory to test workspace + _, cleanUp := sca.CreateTestWorkspace(t, "maven-example-with-wrapper") + err := os.Chmod("mvnw", 0700) + defer cleanUp() + assert.NoError(t, err) + expectedUniqueDeps := []string{ + GavPackageTypeIdentifier + "org.jfrog.test:multi1:3.7-SNAPSHOT", + GavPackageTypeIdentifier + "org.codehaus.plexus:plexus-utils:1.5.1", + GavPackageTypeIdentifier + "org.springframework:spring-beans:2.5.6", + GavPackageTypeIdentifier + "commons-logging:commons-logging:1.1.1", + GavPackageTypeIdentifier + "org.jfrog.test:multi3:3.7-SNAPSHOT", + GavPackageTypeIdentifier + "org.apache.commons:commons-email:1.1", + GavPackageTypeIdentifier + "org.springframework:spring-aop:2.5.6", + GavPackageTypeIdentifier + "org.springframework:spring-core:2.5.6", + GavPackageTypeIdentifier + "org.jfrog.test:multi:3.7-SNAPSHOT", + GavPackageTypeIdentifier + "org.jfrog.test:multi2:3.7-SNAPSHOT", + GavPackageTypeIdentifier + "org.testng:testng:5.9", + GavPackageTypeIdentifier + "hsqldb:hsqldb:1.8.0.10", + GavPackageTypeIdentifier + "junit:junit:3.8.1", + GavPackageTypeIdentifier + "javax.activation:activation:1.1", + GavPackageTypeIdentifier + "javax.mail:mail:1.4", + GavPackageTypeIdentifier + "aopalliance:aopalliance:1.0", + GavPackageTypeIdentifier + "commons-io:commons-io:1.4", + GavPackageTypeIdentifier + "javax.servlet.jsp:jsp-api:2.1", + GavPackageTypeIdentifier + "javax.servlet:servlet-api:2.5", + } + + modulesDependencyTrees, uniqueDeps, err := buildMvnDependencyTree(&DependencyTreeParams{IgnoreConfigFile: true, UseWrapper: true}) + if assert.NoError(t, err) && assert.NotEmpty(t, modulesDependencyTrees) { + assert.ElementsMatch(t, uniqueDeps, expectedUniqueDeps, "First is actual, Second is Expected") + // Check root module + multi := sca.GetAndAssertNode(t, modulesDependencyTrees, "org.jfrog.test:multi:3.7-SNAPSHOT") + if assert.NotNil(t, multi) { + assert.Empty(t, multi.Nodes) + // Check multi1 with a transitive dependency + multi1 := sca.GetAndAssertNode(t, modulesDependencyTrees, "org.jfrog.test:multi1:3.7-SNAPSHOT") + assert.Len(t, multi1.Nodes, 7) + commonsEmail := sca.GetAndAssertNode(t, multi1.Nodes, "org.apache.commons:commons-email:1.1") + assert.Len(t, commonsEmail.Nodes, 2) + // Check multi2 and multi3 + multi2 := sca.GetAndAssertNode(t, modulesDependencyTrees, "org.jfrog.test:multi2:3.7-SNAPSHOT") + assert.Len(t, multi2.Nodes, 1) + multi3 := sca.GetAndAssertNode(t, modulesDependencyTrees, "org.jfrog.test:multi3:3.7-SNAPSHOT") + assert.Len(t, multi3.Nodes, 4) + } + } +} diff --git a/xray/audit/npm/npm.go b/xray/commands/audit/sca/npm/npm.go similarity index 82% rename from xray/audit/npm/npm.go rename to xray/commands/audit/sca/npm/npm.go index cc1041966..7a646e349 100644 --- a/xray/audit/npm/npm.go +++ b/xray/commands/audit/sca/npm/npm.go @@ -4,7 +4,7 @@ import ( biutils "github.com/jfrog/build-info-go/build/utils" buildinfo "github.com/jfrog/build-info-go/entities" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/jfrog/jfrog-cli-core/v2/xray/audit" + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/sca" "github.com/jfrog/jfrog-client-go/utils/log" xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" "golang.org/x/exp/slices" @@ -15,7 +15,7 @@ const ( ignoreScriptsFlag = "--ignore-scripts" ) -func BuildDependencyTree(npmArgs []string) (dependencyTree []*xrayUtils.GraphNode, err error) { +func BuildDependencyTree(npmArgs []string) (dependencyTrees []*xrayUtils.GraphNode, uniqueDeps []string, err error) { currentDir, err := coreutils.GetWorkingDirectory() if err != nil { return @@ -40,9 +40,9 @@ func BuildDependencyTree(npmArgs []string) (dependencyTree []*xrayUtils.GraphNod for _, dependency := range dependenciesMap { dependenciesList = append(dependenciesList, dependency.Dependency) } - // Parse the dependencies into Xray dependency tree format - dependencyTree = []*xrayUtils.GraphNode{parseNpmDependenciesList(dependenciesList, packageInfo)} + dependencyTree, uniqueDeps := parseNpmDependenciesList(dependenciesList, packageInfo) + dependencyTrees = []*xrayUtils.GraphNode{dependencyTree} return } @@ -55,7 +55,7 @@ func addIgnoreScriptsFlag(npmArgs []string) []string { } // Parse the dependencies into an Xray dependency tree format -func parseNpmDependenciesList(dependencies []buildinfo.Dependency, packageInfo *biutils.PackageInfo) (xrDependencyTree *xrayUtils.GraphNode) { +func parseNpmDependenciesList(dependencies []buildinfo.Dependency, packageInfo *biutils.PackageInfo) (*xrayUtils.GraphNode, []string) { treeMap := make(map[string][]string) for _, dependency := range dependencies { dependencyId := npmPackageTypeIdentifier + dependency.Id @@ -68,7 +68,7 @@ func parseNpmDependenciesList(dependencies []buildinfo.Dependency, packageInfo * } } } - return audit.BuildXrayDependencyTree(treeMap, npmPackageTypeIdentifier+packageInfo.BuildInfoModuleId()) + return sca.BuildXrayDependencyTree(treeMap, npmPackageTypeIdentifier+packageInfo.BuildInfoModuleId()) } func appendUniqueChild(children []string, candidateDependency string) []string { diff --git a/xray/audit/npm/npm_test.go b/xray/commands/audit/sca/npm/npm_test.go similarity index 86% rename from xray/audit/npm/npm_test.go rename to xray/commands/audit/sca/npm/npm_test.go index afc415de2..aaa9ea47c 100644 --- a/xray/audit/npm/npm_test.go +++ b/xray/commands/audit/sca/npm/npm_test.go @@ -2,6 +2,7 @@ package npm import ( "encoding/json" + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/sca" xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" "os" "testing" @@ -9,13 +10,12 @@ import ( biutils "github.com/jfrog/build-info-go/build/utils" buildinfo "github.com/jfrog/build-info-go/entities" "github.com/jfrog/jfrog-cli-core/v2/utils/tests" - "github.com/jfrog/jfrog-cli-core/v2/xray/audit" "github.com/stretchr/testify/assert" ) func TestParseNpmDependenciesList(t *testing.T) { // Create and change directory to test workspace - _, cleanUp := audit.CreateTestWorkspace(t, "npm") + _, cleanUp := sca.CreateTestWorkspace(t, "npm") defer cleanUp() dependenciesJson, err := os.ReadFile("dependencies.json") assert.NoError(t, err) @@ -76,12 +76,11 @@ func TestParseNpmDependenciesList(t *testing.T) { }}, {Id: "npm://next:12.0.10", Nodes: []*xrayUtils.GraphNode{ {Id: "npm://react-dom:18.2.0", Nodes: []*xrayUtils.GraphNode{ - {Id: "npm://react:18.2.0", Nodes: looseEnvifyJsTokens}, - {Id: "npm://loose-envify:1.4.0", Nodes: []*xrayUtils.GraphNode{{Id: "npm://js-tokens:4.0.0"}}}, - {Id: "npm://scheduler:0.23.0", Nodes: looseEnvifyJsTokens}}}, + {Id: "npm://react:18.2.0"}, + {Id: "npm://scheduler:0.23.0"}}}, {Id: "npm://styled-jsx:5.0.0"}, {Id: "npm://@next/swc-darwin-arm64:12.0.10"}, - {Id: "npm://react:18.2.0", Nodes: looseEnvifyJsTokens}, + {Id: "npm://react:18.2.0"}, {Id: "npm://@next/env:12.0.10"}, {Id: "npm://caniuse-lite:1.0.30001486"}, {Id: "npm://postcss:8.4.5", Nodes: []*xrayUtils.GraphNode{ @@ -96,21 +95,26 @@ func TestParseNpmDependenciesList(t *testing.T) { }, } - xrayDependenciesTree := parseNpmDependenciesList(dependencies, packageInfo) + xrayDependenciesTree, uniqueDeps := parseNpmDependenciesList(dependencies, packageInfo) equals := tests.CompareTree(expectedTree, xrayDependenciesTree) if !equals { t.Error("expected:", expectedTree.Nodes, "got:", xrayDependenciesTree.Nodes) } + expectedUniqueDeps := []string{xrayDependenciesTree.Id} + for _, dep := range dependencies { + expectedUniqueDeps = append(expectedUniqueDeps, npmPackageTypeIdentifier+dep.Id) + } + assert.ElementsMatch(t, uniqueDeps, expectedUniqueDeps, "First is actual, Second is Expected") } func TestIgnoreScripts(t *testing.T) { // Create and change directory to test workspace - _, cleanUp := audit.CreateTestWorkspace(t, "npm-scripts") + _, cleanUp := sca.CreateTestWorkspace(t, "npm-scripts") defer cleanUp() // The package.json file contain a postinstall script running an "exit 1" command. // Without the "--ignore-scripts" flag, the test will fail. - _, err := BuildDependencyTree([]string{}) + _, _, err := BuildDependencyTree([]string{}) assert.NoError(t, err) } diff --git a/xray/commands/audit/sca/nuget/nuget.go b/xray/commands/audit/sca/nuget/nuget.go new file mode 100644 index 000000000..ba1b293e0 --- /dev/null +++ b/xray/commands/audit/sca/nuget/nuget.go @@ -0,0 +1,105 @@ +package nuget + +import ( + "errors" + "fmt" + "github.com/jfrog/build-info-go/build/utils/dotnet/solution" + "github.com/jfrog/build-info-go/entities" + biutils "github.com/jfrog/build-info-go/utils" + "github.com/jfrog/gofrog/datastructures" + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/sca" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + "github.com/jfrog/jfrog-client-go/utils/io/fileutils" + "github.com/jfrog/jfrog-client-go/utils/log" + xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" + "os" + "os/exec" +) + +const ( + nugetPackageTypeIdentifier = "nuget://" +) + +func BuildDependencyTree() (dependencyTree []*xrayUtils.GraphNode, uniqueDeps []string, err error) { + wd, err := os.Getwd() + if err != nil { + return + } + sol, err := solution.Load(wd, "", log.Logger) + if err != nil { + return + } + + // In case the project's dependencies sources can't be found we run 'dotnet restore' on a copy of the project in order to get its dependencies + if !sol.DependenciesSourcesExist() { + log.Info("Dependencies sources were not detected. Running 'dotnet restore' command") + sol, err = runDotnetRestoreAndLoadSolution(wd) + if err != nil { + return + } + } + + buildInfo, err := sol.BuildInfo("", log.Logger) + if err != nil { + return + } + dependencyTree, uniqueDeps = parseNugetDependencyTree(buildInfo) + return +} + +func runDotnetRestore(wd string) (err error) { + command := exec.Command("dotnet", "restore") + command.Dir = wd + output, err := command.CombinedOutput() + if err != nil { + err = errorutils.CheckErrorf("'dotnet restore' command failed: %s - %s", err.Error(), output) + } + return +} + +func runDotnetRestoreAndLoadSolution(originalWd string) (sol solution.Solution, err error) { + tmpWd, err := fileutils.CreateTempDir() + if err != nil { + err = fmt.Errorf("failed creating temporary dir: %w", err) + return + } + defer func() { + err = errors.Join(err, fileutils.RemoveTempDir(tmpWd)) + }() + + err = biutils.CopyDir(originalWd, tmpWd, true, nil) + if err != nil { + err = fmt.Errorf("failed copying project to temp dir: %w", err) + return + } + + err = runDotnetRestore(tmpWd) + if err != nil { + return + } + sol, err = solution.Load(tmpWd, "", log.Logger) + return +} + +func parseNugetDependencyTree(buildInfo *entities.BuildInfo) (nodes []*xrayUtils.GraphNode, allUniqueDeps []string) { + uniqueDepsSet := datastructures.MakeSet[string]() + for _, module := range buildInfo.Modules { + treeMap := make(map[string][]string) + for _, dependency := range module.Dependencies { + dependencyId := nugetPackageTypeIdentifier + dependency.Id + parent := nugetPackageTypeIdentifier + dependency.RequestedBy[0][0] + if children, ok := treeMap[parent]; ok { + treeMap[parent] = append(children, dependencyId) + } else { + treeMap[parent] = []string{dependencyId} + } + } + dependencyTree, uniqueDeps := sca.BuildXrayDependencyTree(treeMap, nugetPackageTypeIdentifier+module.Id) + nodes = append(nodes, dependencyTree) + for _, uniqueDep := range uniqueDeps { + uniqueDepsSet.Add(uniqueDep) + } + } + allUniqueDeps = uniqueDepsSet.ToSlice() + return +} diff --git a/xray/audit/nuget/nuget_test.go b/xray/commands/audit/sca/nuget/nuget_test.go similarity index 54% rename from xray/audit/nuget/nuget_test.go rename to xray/commands/audit/sca/nuget/nuget_test.go index 2584a865e..90ca883ae 100644 --- a/xray/audit/nuget/nuget_test.go +++ b/xray/commands/audit/sca/nuget/nuget_test.go @@ -2,19 +2,19 @@ package nuget import ( "encoding/json" + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/sca" xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" "os" "testing" "github.com/jfrog/build-info-go/entities" "github.com/jfrog/jfrog-cli-core/v2/utils/tests" - "github.com/jfrog/jfrog-cli-core/v2/xray/audit" "github.com/stretchr/testify/assert" ) func TestBuildNugetDependencyTree(t *testing.T) { // Create and change directory to test workspace - _, cleanUp := audit.CreateTestWorkspace(t, "nuget") + _, cleanUp := sca.CreateTestWorkspace(t, "nuget") defer cleanUp() dependenciesJson, err := os.ReadFile("dependencies.json") assert.NoError(t, err) @@ -22,8 +22,20 @@ func TestBuildNugetDependencyTree(t *testing.T) { var dependencies *entities.BuildInfo err = json.Unmarshal(dependenciesJson, &dependencies) assert.NoError(t, err) - xrayDependenciesTree := parseNugetDependencyTree(dependencies) - + expectedUniqueDeps := []string{ + nugetPackageTypeIdentifier + "Microsoft.Net.Http:2.2.29", + nugetPackageTypeIdentifier + "Microsoft.Bcl:1.1.10", + nugetPackageTypeIdentifier + "Microsoft.Bcl.Build:1.0.14", + nugetPackageTypeIdentifier + "Newtonsoft.Json:11.0.2", + nugetPackageTypeIdentifier + "NUnit:3.10.1", + nugetPackageTypeIdentifier + "bootstrap:4.1.1", + nugetPackageTypeIdentifier + "popper.js:1.14.0", + nugetPackageTypeIdentifier + "jQuery:3.0.0", + nugetPackageTypeIdentifier + "MsbuildExample", + nugetPackageTypeIdentifier + "MsbuildLibrary", + } + xrayDependenciesTree, uniqueDeps := parseNugetDependencyTree(dependencies) + assert.ElementsMatch(t, uniqueDeps, expectedUniqueDeps, "First is actual, Second is Expected") expectedTreeJson, err := os.ReadFile("expectedTree.json") assert.NoError(t, err) diff --git a/xray/audit/python/python.go b/xray/commands/audit/sca/python/python.go similarity index 79% rename from xray/audit/python/python.go rename to xray/commands/audit/sca/python/python.go index 12b3311f6..4efb193ac 100644 --- a/xray/audit/python/python.go +++ b/xray/commands/audit/sca/python/python.go @@ -1,11 +1,15 @@ package python import ( + "errors" "fmt" + biutils "github.com/jfrog/build-info-go/utils" "github.com/jfrog/build-info-go/utils/pythonutils" + "github.com/jfrog/gofrog/datastructures" "github.com/jfrog/jfrog-cli-core/v2/utils/config" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" utils "github.com/jfrog/jfrog-cli-core/v2/utils/python" - "github.com/jfrog/jfrog-cli-core/v2/xray/audit" + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/sca" "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" "github.com/jfrog/jfrog-client-go/utils/log" @@ -28,25 +32,28 @@ type AuditPython struct { PipRequirementsFile string } -func BuildDependencyTree(auditPython *AuditPython) (dependencyTree []*xrayUtils.GraphNode, err error) { +func BuildDependencyTree(auditPython *AuditPython) (dependencyTree []*xrayUtils.GraphNode, uniqueDeps []string, err error) { dependenciesGraph, directDependenciesList, err := getDependencies(auditPython) if err != nil { return } directDependencies := []*xrayUtils.GraphNode{} + uniqueDepsSet := datastructures.MakeSet[string]() for _, rootDep := range directDependenciesList { directDependency := &xrayUtils.GraphNode{ Id: pythonPackageTypeIdentifier + rootDep, Nodes: []*xrayUtils.GraphNode{}, } - populatePythonDependencyTree(directDependency, dependenciesGraph) + populatePythonDependencyTree(directDependency, dependenciesGraph, uniqueDepsSet) directDependencies = append(directDependencies, directDependency) } root := &xrayUtils.GraphNode{ - Id: pythonPackageTypeIdentifier, + Id: "root", Nodes: directDependencies, } - return []*xrayUtils.GraphNode{root}, nil + dependencyTree = []*xrayUtils.GraphNode{root} + uniqueDeps = uniqueDepsSet.ToSlice() + return } func getDependencies(auditPython *AuditPython) (dependenciesGraph map[string][]string, directDependencies []string, err error) { @@ -67,28 +74,21 @@ func getDependencies(auditPython *AuditPython) (dependenciesGraph map[string][]s } defer func() { - e := os.Chdir(wd) - if err == nil { - err = errorutils.CheckError(e) - } - - e = fileutils.RemoveTempDir(tempDirPath) - if err == nil { - err = e - } + err = errors.Join( + err, + errorutils.CheckError(os.Chdir(wd)), + fileutils.RemoveTempDir(tempDirPath), + ) }() - err = fileutils.CopyDir(wd, tempDirPath, true, nil) + err = biutils.CopyDir(wd, tempDirPath, true, nil) if err != nil { return } restoreEnv, err := runPythonInstall(auditPython) defer func() { - e := restoreEnv() - if err == nil { - err = e - } + err = errors.Join(err, restoreEnv()) }() if err != nil { return @@ -100,12 +100,8 @@ func getDependencies(auditPython *AuditPython) (dependenciesGraph map[string][]s } dependenciesGraph, directDependencies, err = pythonutils.GetPythonDependencies(auditPython.Tool, tempDirPath, localDependenciesPath) if err != nil { - if _, innerErr := audit.GetExecutableVersion("python"); innerErr != nil { - log.Error(innerErr) - } - if _, innerErr := audit.GetExecutableVersion(string(auditPython.Tool)); innerErr != nil { - log.Error(innerErr) - } + sca.LogExecutableVersion("python") + sca.LogExecutableVersion(string(auditPython.Tool)) } return } @@ -163,14 +159,19 @@ func installPipDeps(auditPython *AuditPython) (restoreEnv func() error, err erro if err != nil { return } + + remoteUrl := "" if auditPython.RemotePypiRepo != "" { - return restoreEnv, runPipInstallFromRemoteRegistry(auditPython.Server, auditPython.RemotePypiRepo, auditPython.PipRequirementsFile) + remoteUrl, err = utils.GetPypiRepoUrl(auditPython.Server, auditPython.RemotePypiRepo) + if err != nil { + return + } } - pipInstallArgs := getPipInstallArgs(auditPython.PipRequirementsFile) + pipInstallArgs := getPipInstallArgs(auditPython.PipRequirementsFile, remoteUrl) err = executeCommand("python", pipInstallArgs...) if err != nil && auditPython.PipRequirementsFile == "" { log.Debug(err.Error() + "\nTrying to install using a requirements file...") - pipInstallArgs = getPipInstallArgs("requirements.txt") + pipInstallArgs = getPipInstallArgs("requirements.txt", remoteUrl) reqErr := executeCommand("python", pipInstallArgs...) if reqErr != nil { // Return Pip install error and log the requirements fallback error. @@ -184,18 +185,17 @@ func installPipDeps(auditPython *AuditPython) (restoreEnv func() error, err erro func executeCommand(executable string, args ...string) error { installCmd := exec.Command(executable, args...) - log.Debug(fmt.Sprintf("Running %q", strings.Join(installCmd.Args, " "))) + maskedCmdString := coreutils.GetMaskedCommandString(installCmd) + log.Debug("Running", maskedCmdString) output, err := installCmd.CombinedOutput() if err != nil { - if _, innerErr := audit.GetExecutableVersion(executable); innerErr != nil { - log.Error(innerErr) - } - return errorutils.CheckErrorf("%q command failed: %s - %s", strings.Join(installCmd.Args, " "), err.Error(), output) + sca.LogExecutableVersion(executable) + return errorutils.CheckErrorf("%q command failed: %s - %s", maskedCmdString, err.Error(), output) } return nil } -func getPipInstallArgs(requirementsFile string) []string { +func getPipInstallArgs(requirementsFile, remoteUrl string) []string { args := []string{"-m", "pip", "install"} if requirementsFile == "" { // Run 'pip install .' @@ -204,17 +204,10 @@ func getPipInstallArgs(requirementsFile string) []string { // Run pip 'install -r requirements ' args = append(args, "-r", requirementsFile) } - return args -} - -func runPipInstallFromRemoteRegistry(server *config.ServerDetails, depsRepoName, pipRequirementsFile string) (err error) { - rtUrl, err := utils.GetPypiRepoUrl(server, depsRepoName) - if err != nil { - return err + if remoteUrl != "" { + args = append(args, utils.GetPypiRemoteRegistryFlag(pythonutils.Pip), remoteUrl) } - args := getPipInstallArgs(pipRequirementsFile) - args = append(args, utils.GetPypiRemoteRegistryFlag(pythonutils.Pip), rtUrl.String()) - return executeCommand("python", args...) + return args } func runPipenvInstallFromRemoteRegistry(server *config.ServerDetails, depsRepoName string) (err error) { @@ -222,7 +215,7 @@ func runPipenvInstallFromRemoteRegistry(server *config.ServerDetails, depsRepoNa if err != nil { return err } - args := []string{"install", "-d", utils.GetPypiRemoteRegistryFlag(pythonutils.Pipenv), rtUrl.String()} + args := []string{"install", "-d", utils.GetPypiRemoteRegistryFlag(pythonutils.Pipenv), rtUrl} return executeCommand("pipenv", args...) } @@ -271,10 +264,11 @@ func SetPipVirtualEnvPath() (restoreEnv func() error, err error) { return } -func populatePythonDependencyTree(currNode *xrayUtils.GraphNode, dependenciesGraph map[string][]string) { +func populatePythonDependencyTree(currNode *xrayUtils.GraphNode, dependenciesGraph map[string][]string, uniqueDepsSet *datastructures.Set[string]) { if currNode.NodeHasLoop() { return } + uniqueDepsSet.Add(currNode.Id) currDepChildren := dependenciesGraph[strings.TrimPrefix(currNode.Id, pythonPackageTypeIdentifier)] // Recursively create & append all node's dependencies. for _, dependency := range currDepChildren { @@ -284,6 +278,6 @@ func populatePythonDependencyTree(currNode *xrayUtils.GraphNode, dependenciesGra Parent: currNode, } currNode.Nodes = append(currNode.Nodes, childNode) - populatePythonDependencyTree(childNode, dependenciesGraph) + populatePythonDependencyTree(childNode, dependenciesGraph, uniqueDepsSet) } } diff --git a/xray/commands/audit/sca/python/python_test.go b/xray/commands/audit/sca/python/python_test.go new file mode 100644 index 000000000..bf8ae768d --- /dev/null +++ b/xray/commands/audit/sca/python/python_test.go @@ -0,0 +1,144 @@ +package python + +import ( + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/sca" + "path/filepath" + "testing" + + "github.com/jfrog/build-info-go/utils/pythonutils" + "github.com/stretchr/testify/assert" +) + +func TestBuildPipDependencyListSetuppy(t *testing.T) { + // Create and change directory to test workspace + _, cleanUp := sca.CreateTestWorkspace(t, filepath.Join("pip-project", "setuppyproject")) + defer cleanUp() + // Run getModulesDependencyTrees + rootNode, uniqueDeps, err := BuildDependencyTree(&AuditPython{Tool: pythonutils.Pip}) + assert.NoError(t, err) + assert.Contains(t, uniqueDeps, pythonPackageTypeIdentifier+"pexpect:4.8.0") + assert.Contains(t, uniqueDeps, pythonPackageTypeIdentifier+"ptyprocess:0.7.0") + assert.Contains(t, uniqueDeps, pythonPackageTypeIdentifier+"pip-example:1.2.3") + assert.Len(t, rootNode, 1) + if len(rootNode) > 0 { + assert.NotEmpty(t, rootNode[0].Nodes) + if rootNode[0].Nodes != nil { + // Test direct dependency + directDepNode := sca.GetAndAssertNode(t, rootNode[0].Nodes, "pip-example:1.2.3") + // Test child module + childNode := sca.GetAndAssertNode(t, directDepNode.Nodes, "pexpect:4.8.0") + // Test sub child module + sca.GetAndAssertNode(t, childNode.Nodes, "ptyprocess:0.7.0") + } + } +} + +func TestPipDependencyListRequirementsFallback(t *testing.T) { + // Create and change directory to test workspace + _, cleanUp := sca.CreateTestWorkspace(t, filepath.Join("pip-project", "requirementsproject")) + defer cleanUp() + // No requirements file field specified, expect the command to use the fallback 'pip install -r requirements.txt' command + rootNode, uniqueDeps, err := BuildDependencyTree(&AuditPython{Tool: pythonutils.Pip}) + assert.NoError(t, err) + assert.Contains(t, uniqueDeps, pythonPackageTypeIdentifier+"pexpect:4.7.0") + assert.Contains(t, uniqueDeps, pythonPackageTypeIdentifier+"ptyprocess:0.7.0") + assert.Len(t, rootNode, 1) + if assert.True(t, len(rootNode[0].Nodes) > 2) { + childNode := sca.GetAndAssertNode(t, rootNode[0].Nodes, "pexpect:4.7.0") + if childNode != nil { + // Test child module + sca.GetAndAssertNode(t, childNode.Nodes, "ptyprocess:0.7.0") + } + } +} + +func TestBuildPipDependencyListRequirements(t *testing.T) { + // Create and change directory to test workspace + _, cleanUp := sca.CreateTestWorkspace(t, filepath.Join("pip-project", "requirementsproject")) + defer cleanUp() + // Run getModulesDependencyTrees + rootNode, uniqueDeps, err := BuildDependencyTree(&AuditPython{Tool: pythonutils.Pip, PipRequirementsFile: "requirements.txt"}) + assert.NoError(t, err) + assert.Contains(t, uniqueDeps, pythonPackageTypeIdentifier+"pexpect:4.7.0") + assert.Contains(t, uniqueDeps, pythonPackageTypeIdentifier+"ptyprocess:0.7.0") + assert.Len(t, rootNode, 1) + if len(rootNode) > 0 { + assert.NotEmpty(t, rootNode[0].Nodes) + if rootNode[0].Nodes != nil { + // Test root module + directDepNode := sca.GetAndAssertNode(t, rootNode[0].Nodes, "pexpect:4.7.0") + // Test child module + sca.GetAndAssertNode(t, directDepNode.Nodes, "ptyprocess:0.7.0") + } + } +} + +func TestBuildPipenvDependencyList(t *testing.T) { + // Create and change directory to test workspace + _, cleanUp := sca.CreateTestWorkspace(t, "pipenv-project") + defer cleanUp() + expectedPipenvUniqueDeps := []string{ + pythonPackageTypeIdentifier + "toml:0.10.2", + pythonPackageTypeIdentifier + "pexpect:4.8.0", + pythonPackageTypeIdentifier + "ptyprocess:0.7.0", + } + // Run getModulesDependencyTrees + rootNode, uniqueDeps, err := BuildDependencyTree(&AuditPython{Tool: pythonutils.Pipenv}) + if err != nil { + t.Fatal(err) + } + assert.ElementsMatch(t, uniqueDeps, expectedPipenvUniqueDeps, "First is actual, Second is Expected") + assert.Len(t, rootNode, 1) + if len(rootNode) > 0 { + assert.NotEmpty(t, rootNode[0].Nodes) + // Test child module + childNode := sca.GetAndAssertNode(t, rootNode[0].Nodes, "pexpect:4.8.0") + // Test sub child module + if assert.NotNil(t, childNode) { + sca.GetAndAssertNode(t, childNode.Nodes, "ptyprocess:0.7.0") + } + } +} + +func TestBuildPoetryDependencyList(t *testing.T) { + // Create and change directory to test workspace + _, cleanUp := sca.CreateTestWorkspace(t, "poetry-project") + defer cleanUp() + expectedPoetryUniqueDeps := []string{ + pythonPackageTypeIdentifier + "wcwidth:0.2.5", + pythonPackageTypeIdentifier + "colorama:0.4.6", + pythonPackageTypeIdentifier + "packaging:22.0", + pythonPackageTypeIdentifier + "python:", + pythonPackageTypeIdentifier + "pluggy:0.13.1", + pythonPackageTypeIdentifier + "py:1.11.0", + pythonPackageTypeIdentifier + "atomicwrites:1.4.1", + pythonPackageTypeIdentifier + "attrs:22.1.0", + pythonPackageTypeIdentifier + "more-itertools:9.0.0", + pythonPackageTypeIdentifier + "numpy:1.24.0", + pythonPackageTypeIdentifier + "pytest:5.4.3", + } + // Run getModulesDependencyTrees + rootNode, uniqueDeps, err := BuildDependencyTree(&AuditPython{Tool: pythonutils.Poetry}) + if err != nil { + t.Fatal(err) + } + assert.ElementsMatch(t, uniqueDeps, expectedPoetryUniqueDeps, "First is actual, Second is Expected") + assert.Len(t, rootNode, 1) + if len(rootNode) > 0 { + assert.NotEmpty(t, rootNode[0].Nodes) + // Test child module + childNode := sca.GetAndAssertNode(t, rootNode[0].Nodes, "pytest:5.4.3") + // Test sub child module + if assert.NotNil(t, childNode) { + sca.GetAndAssertNode(t, childNode.Nodes, "packaging:22.0") + } + } +} + +func TestGetPipInstallArgs(t *testing.T) { + assert.Equal(t, []string{"-m", "pip", "install", "."}, getPipInstallArgs("", "")) + assert.Equal(t, []string{"-m", "pip", "install", "-r", "requirements.txt"}, getPipInstallArgs("requirements.txt", "")) + + assert.Equal(t, []string{"-m", "pip", "install", ".", "-i", "https://user@pass:remote.url/repo"}, getPipInstallArgs("", "https://user@pass:remote.url/repo")) + assert.Equal(t, []string{"-m", "pip", "install", "-r", "requirements.txt", "-i", "https://user@pass:remote.url/repo"}, getPipInstallArgs("requirements.txt", "https://user@pass:remote.url/repo")) +} diff --git a/xray/audit/yarn/yarn.go b/xray/commands/audit/sca/yarn/yarn.go similarity index 78% rename from xray/audit/yarn/yarn.go rename to xray/commands/audit/sca/yarn/yarn.go index 12d987982..9df1333c9 100644 --- a/xray/audit/yarn/yarn.go +++ b/xray/commands/audit/sca/yarn/yarn.go @@ -3,7 +3,7 @@ package yarn import ( biUtils "github.com/jfrog/build-info-go/build/utils" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/jfrog/jfrog-cli-core/v2/xray/audit" + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/sca" "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/log" xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" @@ -13,7 +13,7 @@ const ( npmPackageTypeIdentifier = "npm://" ) -func BuildDependencyTree() (dependencyTree []*xrayUtils.GraphNode, err error) { +func BuildDependencyTree() (dependencyTrees []*xrayUtils.GraphNode, uniqueDeps []string, err error) { currentDir, err := coreutils.GetWorkingDirectory() if err != nil { return @@ -33,12 +33,13 @@ func BuildDependencyTree() (dependencyTree []*xrayUtils.GraphNode, err error) { return } // Parse the dependencies into Xray dependency tree format - dependencyTree = []*xrayUtils.GraphNode{parseYarnDependenciesMap(dependenciesMap, getXrayDependencyId(root))} + dependencyTree, uniqueDeps := parseYarnDependenciesMap(dependenciesMap, getXrayDependencyId(root)) + dependencyTrees = []*xrayUtils.GraphNode{dependencyTree} return } // Parse the dependencies into a Xray dependency tree format -func parseYarnDependenciesMap(dependencies map[string]*biUtils.YarnDependency, rootXrayId string) (xrDependencyTree *xrayUtils.GraphNode) { +func parseYarnDependenciesMap(dependencies map[string]*biUtils.YarnDependency, rootXrayId string) (*xrayUtils.GraphNode, []string) { treeMap := make(map[string][]string) for _, dependency := range dependencies { xrayDepId := getXrayDependencyId(dependency) @@ -50,7 +51,7 @@ func parseYarnDependenciesMap(dependencies map[string]*biUtils.YarnDependency, r treeMap[xrayDepId] = subDeps } } - return audit.BuildXrayDependencyTree(treeMap, rootXrayId) + return sca.BuildXrayDependencyTree(treeMap, rootXrayId) } func getXrayDependencyId(yarnDependency *biUtils.YarnDependency) string { diff --git a/xray/audit/yarn/yarn_test.go b/xray/commands/audit/sca/yarn/yarn_test.go similarity index 69% rename from xray/audit/yarn/yarn_test.go rename to xray/commands/audit/sca/yarn/yarn_test.go index c446bb907..bb24f3c0d 100644 --- a/xray/audit/yarn/yarn_test.go +++ b/xray/commands/audit/sca/yarn/yarn_test.go @@ -18,26 +18,33 @@ func TestParseYarnDependenciesList(t *testing.T) { "pack5@npm:5.0.0": {Value: "pack5@npm:5.0.0", Details: biutils.YarnDepDetails{Version: "5.0.0", Dependencies: []biutils.YarnDependencyPointer{{Locator: "pack2@npm:2.0.0"}}}}, } - rootXrayId := "npm://@jfrog/pack3:3.0.0" + rootXrayId := npmPackageTypeIdentifier + "@jfrog/pack3:3.0.0" expectedTree := &xrayUtils.GraphNode{ Id: rootXrayId, Nodes: []*xrayUtils.GraphNode{ - {Id: "npm://pack1:1.0.0", + {Id: npmPackageTypeIdentifier + "pack1:1.0.0", Nodes: []*xrayUtils.GraphNode{ - {Id: "npm://pack4:4.0.0", + {Id: npmPackageTypeIdentifier + "pack4:4.0.0", Nodes: []*xrayUtils.GraphNode{}}, }}, - {Id: "npm://pack2:2.0.0", + {Id: npmPackageTypeIdentifier + "pack2:2.0.0", Nodes: []*xrayUtils.GraphNode{ - {Id: "npm://pack4:4.0.0", + {Id: npmPackageTypeIdentifier + "pack4:4.0.0", Nodes: []*xrayUtils.GraphNode{}}, - {Id: "npm://pack5:5.0.0", + {Id: npmPackageTypeIdentifier + "pack5:5.0.0", Nodes: []*xrayUtils.GraphNode{}}, }}, }, } + expectedUniqueDeps := []string{ + npmPackageTypeIdentifier + "pack1:1.0.0", + npmPackageTypeIdentifier + "pack2:2.0.0", + npmPackageTypeIdentifier + "pack4:4.0.0", + npmPackageTypeIdentifier + "pack5:5.0.0", + npmPackageTypeIdentifier + "@jfrog/pack3:3.0.0", + } - xrayDependenciesTree := parseYarnDependenciesMap(yarnDependencies, rootXrayId) - + xrayDependenciesTree, uniqueDeps := parseYarnDependenciesMap(yarnDependencies, rootXrayId) + assert.ElementsMatch(t, uniqueDeps, expectedUniqueDeps, "First is actual, Second is Expected") assert.True(t, tests.CompareTree(expectedTree, xrayDependenciesTree), "expected:", expectedTree.Nodes, "got:", xrayDependenciesTree.Nodes) } diff --git a/xray/commands/audit/scarunner.go b/xray/commands/audit/scarunner.go new file mode 100644 index 000000000..3f8e6144e --- /dev/null +++ b/xray/commands/audit/scarunner.go @@ -0,0 +1,183 @@ +package audit + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/jfrog/build-info-go/utils/pythonutils" + "github.com/jfrog/gofrog/datastructures" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/sca" + _go "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/sca/go" + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/sca/java" + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/sca/npm" + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/sca/nuget" + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/sca/python" + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/sca/yarn" + "github.com/jfrog/jfrog-cli-core/v2/xray/scangraph" + xrayutils "github.com/jfrog/jfrog-cli-core/v2/xray/utils" + clientutils "github.com/jfrog/jfrog-client-go/utils" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + "github.com/jfrog/jfrog-client-go/utils/log" + xrayCmdUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" + "os" + "time" +) + +func runScaScan(params *AuditParams, results *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 *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 + } + + 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 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) + var directDependencies []string + if tech == coreutils.Pip { + // 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. + directDependencies = getDirectDependenciesFromTree([]*xrayCmdUtils.GraphNode{flattenTree}) + } else { + directDependencies = getDirectDependenciesFromTree(fullDependencyTrees) + } + params.AppendDirectDependencies(directDependencies) + + 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 +} + +// This function retrieves the dependency trees of the scanned project and extracts a set that contains only the direct dependencies. +func getDirectDependenciesFromTree(dependencyTrees []*xrayCmdUtils.GraphNode) []string { + directDependencies := datastructures.MakeSet[string]() + for _, tree := range dependencyTrees { + for _, node := range tree.Nodes { + directDependencies.Add(node.Id) + } + } + return directDependencies.ToSlice() +} + +func GetTechDependencyTree(params *xrayutils.AuditBasicParams, tech coreutils.Technology) (flatTree *xrayCmdUtils.GraphNode, fullDependencyTrees []*xrayCmdUtils.GraphNode, err error) { + logMessage := fmt.Sprintf("Calculating %s dependencies", tech.ToFormal()) + log.Info(logMessage) + if params.Progress() != nil { + params.Progress().SetHeadlineMsg(logMessage) + } + serverDetails, err := params.ServerDetails() + if err != nil { + return + } + var uniqueDeps []string + startTime := time.Now() + switch tech { + case coreutils.Maven, coreutils.Gradle: + fullDependencyTrees, uniqueDeps, err = java.BuildDependencyTree(params, tech) + case coreutils.Npm: + fullDependencyTrees, uniqueDeps, err = npm.BuildDependencyTree(params.Args()) + case coreutils.Yarn: + fullDependencyTrees, uniqueDeps, err = yarn.BuildDependencyTree() + case coreutils.Go: + fullDependencyTrees, uniqueDeps, err = _go.BuildDependencyTree(serverDetails, params.DepsRepo()) + case coreutils.Pipenv, coreutils.Pip, coreutils.Poetry: + fullDependencyTrees, uniqueDeps, err = python.BuildDependencyTree(&python.AuditPython{ + Server: serverDetails, + Tool: pythonutils.PythonTool(tech), + RemotePypiRepo: params.DepsRepo(), + PipRequirementsFile: params.PipRequirementsFile()}) + case coreutils.Nuget: + fullDependencyTrees, uniqueDeps, err = nuget.BuildDependencyTree() + default: + err = errorutils.CheckErrorf("%s is currently not supported", string(tech)) + } + if err != nil { + return + } + log.Debug(fmt.Sprintf("Created '%s' dependency tree with %d nodes. Elapsed time: %.1f seconds.", tech.ToFormal(), len(uniqueDeps), time.Since(startTime).Seconds())) + flatTree, err = createFlatTree(uniqueDeps) + return +} + +func createFlatTree(uniqueDeps []string) (*xrayCmdUtils.GraphNode, error) { + if log.GetLogger().GetLogLevel() == log.DEBUG { + // Avoid printing and marshalling if not on DEBUG mode. + jsonList, err := json.Marshal(uniqueDeps) + if errorutils.CheckError(err) != nil { + return nil, err + } + log.Debug("Unique dependencies list:\n" + clientutils.IndentJsonArray(jsonList)) + } + uniqueNodes := []*xrayCmdUtils.GraphNode{} + for _, uniqueDep := range uniqueDeps { + uniqueNodes = append(uniqueNodes, &xrayCmdUtils.GraphNode{Id: uniqueDep}) + } + return &xrayCmdUtils.GraphNode{Id: "root", Nodes: uniqueNodes}, nil +} diff --git a/xray/commands/audit/generic/auditmanager_test.go b/xray/commands/audit/scarunner_test.go similarity index 100% rename from xray/commands/audit/generic/auditmanager_test.go rename to xray/commands/audit/scarunner_test.go diff --git a/xray/commands/curation/audit.go b/xray/commands/curation/curationaudit.go similarity index 96% rename from xray/commands/curation/audit.go rename to xray/commands/curation/curationaudit.go index 9529f59d9..b3d08a1a1 100644 --- a/xray/commands/curation/audit.go +++ b/xray/commands/curation/curationaudit.go @@ -8,8 +8,7 @@ import ( "github.com/jfrog/gofrog/parallel" rtUtils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - audit "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/generic" - cmdUtils "github.com/jfrog/jfrog-cli-core/v2/xray/commands/utils" + "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit" "github.com/jfrog/jfrog-cli-core/v2/xray/utils" "github.com/jfrog/jfrog-client-go/artifactory" "github.com/jfrog/jfrog-client-go/auth" @@ -45,6 +44,8 @@ const ( errorTemplateUnsupportedTech = "It looks like this project uses '%s' to download its dependencies. " + "This package manager however isn't supported by this command." + + TotalConcurrentRequests = 10 ) var supportedTech = map[coreutils.Technology]struct{}{ @@ -110,13 +111,13 @@ type CurationAuditCommand struct { workingDirs []string OriginPath string parallelRequests int - *utils.GraphBasicParams + *utils.AuditBasicParams } func NewCurationAuditCommand() *CurationAuditCommand { return &CurationAuditCommand{ extractPoliciesRegex: regexp.MustCompile(extractPoliciesRegexTemplate), - GraphBasicParams: &utils.GraphBasicParams{}, + AuditBasicParams: &utils.AuditBasicParams{}, } } @@ -178,7 +179,7 @@ func (ca *CurationAuditCommand) Run() (err error) { } func (ca *CurationAuditCommand) doCurateAudit(results map[string][]*PackageStatus) error { - techs := cmdUtils.DetectedTechnologies() + techs := coreutils.DetectedTechnologiesList() for _, tech := range techs { if _, ok := supportedTech[coreutils.Technology(tech)]; !ok { log.Info(fmt.Sprintf(errorTemplateUnsupportedTech, tech)) @@ -192,7 +193,7 @@ func (ca *CurationAuditCommand) doCurateAudit(results map[string][]*PackageStatu } func (ca *CurationAuditCommand) auditTree(tech coreutils.Technology, results map[string][]*PackageStatus) error { - flattenGraph, fullDependenciesTree, err := audit.GetTechDependencyTree(ca.GraphBasicParams, tech) + flattenGraph, fullDependenciesTree, err := audit.GetTechDependencyTree(ca.AuditBasicParams, tech) if err != nil { return err } @@ -219,13 +220,13 @@ func (ca *CurationAuditCommand) auditTree(tech coreutils.Technology, results map rootNode := fullDependenciesTree[0] _, projectName, projectScope, projectVersion := getUrlNameAndVersionByTech(tech, rootNode.Id, "", "") if ca.Progress() != nil { - ca.Progress().SetHeadlineMsg(fmt.Sprintf("Fetch curation status for %s graph with %v nodes project name: %s:%s", tech.ToFormal(), len(flattenGraph[0].Nodes)-1, projectName, projectVersion)) + ca.Progress().SetHeadlineMsg(fmt.Sprintf("Fetch curation status for %s graph with %v nodes project name: %s:%s", tech.ToFormal(), len(flattenGraph.Nodes)-1, projectName, projectVersion)) } if projectScope != "" { projectName = projectScope + "/" + projectName } if ca.parallelRequests == 0 { - ca.parallelRequests = cmdUtils.TotalConcurrentRequests + ca.parallelRequests = TotalConcurrentRequests } var packagesStatus []*PackageStatus analyzer := treeAnalyzer{ @@ -240,7 +241,7 @@ func (ca *CurationAuditCommand) auditTree(tech coreutils.Technology, results map } packagesStatusMap := sync.Map{} // Fetch status for each node from a flatten graph which, has no duplicate nodes. - err = analyzer.fetchNodesStatus(flattenGraph[0], &packagesStatusMap, rootNode.Id) + err = analyzer.fetchNodesStatus(flattenGraph, &packagesStatusMap, rootNode.Id) analyzer.fillGraphRelations(rootNode, &packagesStatusMap, &packagesStatus, "", "", datastructures.MakeSet[string](), true) sort.Slice(packagesStatus, func(i, j int) bool { @@ -533,3 +534,10 @@ func buildNpmDownloadUrl(url, repo, name, scope, version string) string { } return packageUrl } + +func DetectNumOfThreads(threadsCount int) (int, error) { + if threadsCount > TotalConcurrentRequests { + return 0, errorutils.CheckErrorf("number of threads crossed the maximum, the maximum threads allowed is %v", TotalConcurrentRequests) + } + return threadsCount, nil +} diff --git a/xray/commands/curation/audit_test.go b/xray/commands/curation/curationaudit_test.go similarity index 94% rename from xray/commands/curation/audit_test.go rename to xray/commands/curation/curationaudit_test.go index 9a6a81377..6b52fe9c7 100644 --- a/xray/commands/curation/audit_test.go +++ b/xray/commands/curation/curationaudit_test.go @@ -4,12 +4,13 @@ import ( "encoding/json" "fmt" "github.com/jfrog/gofrog/datastructures" - tests2 "github.com/jfrog/jfrog-cli-core/v2/common/tests" + coretests "github.com/jfrog/jfrog-cli-core/v2/common/tests" "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-client-go/utils/io/fileutils" + clienttestutils "github.com/jfrog/jfrog-client-go/utils/tests" xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "net/http" "net/http/httptest" "os" @@ -25,7 +26,7 @@ import ( func TestExtractPoliciesFromMsg(t *testing.T) { var err error extractPoliciesRegex := regexp.MustCompile(extractPoliciesRegexTemplate) - require.NoError(t, err) + assert.NoError(t, err) tests := getTestCasesForExtractPoliciesFromMsg() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -399,40 +400,36 @@ func TestDoCurationAudit(t *testing.T) { tests := getTestCasesForDoCurationAudit() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cliHomeDirBefore := os.Getenv(coreutils.HomeDir) - defer os.Setenv(coreutils.HomeDir, cliHomeDirBefore) currentDir, err := os.Getwd() - require.NoError(t, err) + assert.NoError(t, err) configurationDir := filepath.Join("..", "testdata", "npm-project", ".jfrog") - require.NoError(t, os.Setenv(coreutils.HomeDir, filepath.Join(currentDir, configurationDir))) + callback := clienttestutils.SetEnvWithCallbackAndAssert(t, coreutils.HomeDir, filepath.Join(currentDir, configurationDir)) + defer callback() mockServer, config := curationServer(t, tt.expectedRequest, tt.requestToFail, tt.requestToError) defer mockServer.Close() configFilePath := WriteServerDetailsConfigFileBytes(t, config.ArtifactoryUrl, configurationDir) defer func() { - require.NoError(t, os.Remove(configFilePath)) - require.NoError(t, os.RemoveAll(filepath.Join(configFilePath, "backup"))) + assert.NoError(t, fileutils.RemoveTempDir(configFilePath)) }() curationCmd := NewCurationAuditCommand() curationCmd.parallelRequests = 3 rootDir, err := os.Getwd() - require.NoError(t, err) - defer func() { - require.NoError(t, os.Chdir(rootDir)) - }() + assert.NoError(t, err) // Set the working dir for npm project. - require.NoError(t, os.Chdir("../testdata/npm-project")) + callback = clienttestutils.ChangeDirWithCallback(t, rootDir, "../testdata/npm-project") + defer callback() results := map[string][]*PackageStatus{} if tt.requestToError == nil { - require.NoError(t, curationCmd.doCurateAudit(results)) + assert.NoError(t, curationCmd.doCurateAudit(results)) } else { gotError := curationCmd.doCurateAudit(results) - require.Error(t, gotError) + assert.Error(t, gotError) startUrl := strings.Index(tt.expectedError, "/") - require.GreaterOrEqual(t, startUrl, 0) + assert.GreaterOrEqual(t, startUrl, 0) errMsgExpected := tt.expectedError[:startUrl] + config.ArtifactoryUrl + tt.expectedError[strings.Index(tt.expectedError, "/")+1:] - assert.Equal(t, errMsgExpected, gotError.Error()) + assert.EqualError(t, gotError, errMsgExpected) } // Add the mock server to the expected blocked message url for index := range tt.expectedResp { @@ -533,7 +530,7 @@ func getTestCasesForDoCurationAudit() []struct { func curationServer(t *testing.T, expectedRequest map[string]bool, requestToFail map[string]bool, requestToError map[string]bool) (*httptest.Server, *config.ServerDetails) { mapLockReadWrite := sync.Mutex{} - serverMock, config, _ := tests2.CreateRtRestsMockServer(t, func(w http.ResponseWriter, r *http.Request) { + serverMock, config, _ := coretests.CreateRtRestsMockServer(t, func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodHead { mapLockReadWrite.Lock() if _, exist := expectedRequest[r.RequestURI]; exist { @@ -553,7 +550,7 @@ func curationServer(t *testing.T, expectedRequest map[string]bool, requestToFail _, err := w.Write([]byte("{\n \"errors\": [\n {\n \"status\": 403,\n " + "\"message\": \"Package download was blocked by JFrog Packages " + "Curation service due to the following policies violated {pol1, cond1}\"\n }\n ]\n}")) - require.NoError(t, err) + assert.NoError(t, err) } } }) @@ -575,8 +572,8 @@ func WriteServerDetailsConfigFileBytes(t *testing.T, url string, configPath stri } detailsByte, err := json.Marshal(serverDetails) - require.NoError(t, err) + assert.NoError(t, err) confFilePath := filepath.Join(configPath, "jfrog-cli.conf.v"+strconv.Itoa(coreutils.GetCliConfigVersion())) - require.NoError(t, os.WriteFile(confFilePath, detailsByte, 0644)) + assert.NoError(t, os.WriteFile(confFilePath, detailsByte, 0644)) return confFilePath } diff --git a/xray/commands/scan/buildscan.go b/xray/commands/scan/buildscan.go index 11aa3617f..7dd6bdcd0 100644 --- a/xray/commands/scan/buildscan.go +++ b/xray/commands/scan/buildscan.go @@ -4,7 +4,7 @@ import ( "errors" rtutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" "github.com/jfrog/jfrog-cli-core/v2/utils/config" - "github.com/jfrog/jfrog-cli-core/v2/xray/commands/utils" + "github.com/jfrog/jfrog-cli-core/v2/xray/utils" xrutils "github.com/jfrog/jfrog-cli-core/v2/xray/utils" clientutils "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/log" diff --git a/xray/commands/scan/dockerscan.go b/xray/commands/scan/dockerscan.go index cf840a4d7..462c65f4f 100644 --- a/xray/commands/scan/dockerscan.go +++ b/xray/commands/scan/dockerscan.go @@ -4,7 +4,7 @@ import ( "bytes" "fmt" "github.com/jfrog/jfrog-cli-core/v2/common/spec" - "github.com/jfrog/jfrog-cli-core/v2/xray/commands/utils" + xrayutils "github.com/jfrog/jfrog-cli-core/v2/xray/utils" clientutils "github.com/jfrog/jfrog-client-go/utils" "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" @@ -42,7 +42,7 @@ func (dsc *DockerScanCommand) SetTargetRepoPath(repoPath string) *DockerScanComm func (dsc *DockerScanCommand) Run() (err error) { // Validate Xray minimum version - _, xrayVersion, err := utils.CreateXrayServiceManagerAndGetVersion(dsc.ScanCommand.serverDetails) + _, xrayVersion, err := xrayutils.CreateXrayServiceManagerAndGetVersion(dsc.ScanCommand.serverDetails) if err != nil { return err } diff --git a/xray/utils/downloadindexer.go b/xray/commands/scan/downloadindexer.go similarity index 99% rename from xray/utils/downloadindexer.go rename to xray/commands/scan/downloadindexer.go index 0859febfe..d0c3456d9 100644 --- a/xray/utils/downloadindexer.go +++ b/xray/commands/scan/downloadindexer.go @@ -1,4 +1,4 @@ -package utils +package scan import ( "errors" diff --git a/xray/utils/downloadindexer_test.go b/xray/commands/scan/downloadindexer_test.go similarity index 99% rename from xray/utils/downloadindexer_test.go rename to xray/commands/scan/downloadindexer_test.go index a62dd6e92..0cc7e7b82 100644 --- a/xray/utils/downloadindexer_test.go +++ b/xray/commands/scan/downloadindexer_test.go @@ -1,4 +1,4 @@ -package utils +package scan import ( "github.com/jfrog/jfrog-cli-core/v2/utils/tests" diff --git a/xray/commands/scan/scan.go b/xray/commands/scan/scan.go index e918da3be..b77a3c069 100644 --- a/xray/commands/scan/scan.go +++ b/xray/commands/scan/scan.go @@ -5,7 +5,7 @@ import ( "encoding/json" "errors" "fmt" - "github.com/jfrog/jfrog-cli-core/v2/xray/commands/utils" + "github.com/jfrog/jfrog-cli-core/v2/xray/scangraph" xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" "os/exec" "path/filepath" @@ -31,8 +31,9 @@ type FileContext func(string) parallel.TaskFunc type indexFileHandlerFunc func(file string) const ( - indexingCommand = "graph" - fileNotSupportedExitCode = 3 + BypassArchiveLimitsMinXrayVersion = "3.59.0" + indexingCommand = "graph" + fileNotSupportedExitCode = 3 ) type ScanCommand struct { @@ -128,8 +129,8 @@ func (scanCmd *ScanCommand) SetBypassArchiveLimits(bypassArchiveLimits bool) *Sc return scanCmd } -func (scanCmd *ScanCommand) indexFile(filePath string) (*xrayUtils.GraphNode, error) { - var indexerResults xrayUtils.GraphNode +func (scanCmd *ScanCommand) indexFile(filePath string) (*xrayUtils.BinaryGraphNode, error) { + var indexerResults xrayUtils.BinaryGraphNode indexerCmd := exec.Command(scanCmd.indexerPath, indexingCommand, filePath, "--temp-dir", scanCmd.indexerTempDir) if scanCmd.bypassArchiveLimits { indexerCmd.Args = append(indexerCmd.Args, "--bypass-archive-limits") @@ -140,7 +141,8 @@ func (scanCmd *ScanCommand) indexFile(filePath string) (*xrayUtils.GraphNode, er indexerCmd.Stderr = &stderr err := indexerCmd.Run() if err != nil { - if e, ok := err.(*exec.ExitError); ok { + var e *exec.ExitError + if errors.As(err, &e) { if e.ExitCode() == fileNotSupportedExitCode { log.Debug(fmt.Sprintf("File %s is not supported by Xray indexer app.", filePath)) return &indexerResults, nil @@ -158,34 +160,35 @@ func (scanCmd *ScanCommand) indexFile(filePath string) (*xrayUtils.GraphNode, er func (scanCmd *ScanCommand) Run() (err error) { defer func() { if err != nil { - if e, ok := err.(*exec.ExitError); ok { + var e *exec.ExitError + if errors.As(err, &e) { if e.ExitCode() != coreutils.ExitCodeVulnerableBuild.Code { err = errors.New("Scan command failed. " + err.Error()) } } } }() - xrayManager, xrayVersion, err := utils.CreateXrayServiceManagerAndGetVersion(scanCmd.serverDetails) + xrayManager, xrayVersion, err := xrutils.CreateXrayServiceManagerAndGetVersion(scanCmd.serverDetails) if err != nil { return err } // Validate Xray minimum version for graph scan command - err = clientutils.ValidateMinimumVersion(clientutils.Xray, xrayVersion, utils.GraphScanMinXrayVersion) + err = clientutils.ValidateMinimumVersion(clientutils.Xray, xrayVersion, scangraph.GraphScanMinXrayVersion) if err != nil { return err } if scanCmd.bypassArchiveLimits { // Validate Xray minimum version for BypassArchiveLimits flag for indexer - err = clientutils.ValidateMinimumVersion(clientutils.Xray, xrayVersion, utils.BypassArchiveLimitsMinXrayVersion) + err = clientutils.ValidateMinimumVersion(clientutils.Xray, xrayVersion, BypassArchiveLimitsMinXrayVersion) if err != nil { return err } } log.Info("JFrog Xray version is:", xrayVersion) // First download Xray Indexer if needed - scanCmd.indexerPath, err = xrutils.DownloadIndexerIfNeeded(xrayManager, xrayVersion) + scanCmd.indexerPath, err = DownloadIndexerIfNeeded(xrayManager, xrayVersion) if err != nil { return err } @@ -314,7 +317,7 @@ func (scanCmd *ScanCommand) createIndexerHandlerFunc(file *spec.File, indexedFil // which will send the indexed binary to Xray and then will store the received result. taskFunc := func(threadId int) (err error) { params := &services.XrayGraphScanParams{ - Graph: graph, + BinaryGraph: graph, RepoPath: getXrayRepoPathFromTarget(file.Target), Watches: scanCmd.watches, IncludeLicenses: scanCmd.includeLicenses, @@ -325,13 +328,13 @@ func (scanCmd *ScanCommand) createIndexerHandlerFunc(file *spec.File, indexedFil if scanCmd.progress != nil { scanCmd.progress.SetHeadlineMsg("Scanning 🔍") } - scanGraphParams := utils.NewScanGraphParams(). + scanGraphParams := scangraph.NewScanGraphParams(). SetServerDetails(scanCmd.serverDetails). SetXrayGraphScanParams(params). SetXrayVersion(xrayVersion). SetFixableOnly(scanCmd.fixableOnly). SetSeverityLevel(scanCmd.minSeverityFilter) - scanResults, err := utils.RunScanGraphAndGetResults(scanGraphParams) + scanResults, err := scangraph.RunScanGraphAndGetResults(scanGraphParams) if err != nil { log.Error(fmt.Sprintf("scanning '%s' failed with error: %s", graph.Id, err.Error())) indexedFileErrors[threadId] = append(indexedFileErrors[threadId], formats.SimpleJsonError{FilePath: filePath, ErrorMessage: err.Error()}) diff --git a/xray/commands/testdata/applicability-scan/applicable-cve-results.sarif b/xray/commands/testdata/applicability-scan/applicable-cve-results.sarif index 71b97e5d6..66aee38a5 100644 --- a/xray/commands/testdata/applicability-scan/applicable-cve-results.sarif +++ b/xray/commands/testdata/applicability-scan/applicable-cve-results.sarif @@ -6,7 +6,7 @@ "name": "JFrog Applicability Scanner", "rules": [ { - "id": "applic_CVE-2021-3807", + "id": "applic_testCve1", "fullDescription": { "text": "The scanner checks whether the vulnerable function `ansi-regex` is called.", "markdown": "The scanner checks whether the vulnerable function `ansi-regex` is called." @@ -17,7 +17,7 @@ } }, { - "id": "applic_CVE-2021-3918", + "id": "applic_testCve3", "fullDescription": { "text": "The scanner checks whether any of the following vulnerable functions are called:\n\n* `json-schema.validate` with external input to its 1st (`instance`) argument.\n* `json-schema.checkPropertyChange` with external input to its 2nd (`schema`) argument.", "markdown": "The scanner checks whether any of the following vulnerable functions are called:\n\n* `json-schema.validate` with external input to its 1st (`instance`) argument.\n* `json-schema.checkPropertyChange` with external input to its 2nd (`schema`) argument." diff --git a/xray/commands/testdata/applicability-scan/no-applicable-cves-results.sarif b/xray/commands/testdata/applicability-scan/no-applicable-cves-results.sarif index a0f9cf39e..4257bc869 100644 --- a/xray/commands/testdata/applicability-scan/no-applicable-cves-results.sarif +++ b/xray/commands/testdata/applicability-scan/no-applicable-cves-results.sarif @@ -6,7 +6,7 @@ "name": "JFrog Applicability Scanner", "rules": [ { - "id": "applic_CVE-2021-3807", + "id": "applic_testCve2", "fullDescription": { "text": "The scanner checks whether the vulnerable function `ansi-regex` is called.", "markdown": "The scanner checks whether the vulnerable function `ansi-regex` is called." @@ -17,7 +17,7 @@ } }, { - "id": "applic_CVE-2021-3918", + "id": "applic_testCve3", "fullDescription": { "text": "The scanner checks whether any of the following vulnerable functions are called:\n\n* `json-schema.validate` with external input to its 1st (`instance`) argument.\n* `json-schema.checkPropertyChange` with external input to its 2nd (`schema`) argument.", "markdown": "The scanner checks whether any of the following vulnerable functions are called:\n\n* `json-schema.validate` with external input to its 1st (`instance`) argument.\n* `json-schema.checkPropertyChange` with external input to its 2nd (`schema`) argument." @@ -26,6 +26,39 @@ "shortDescription": { "text": "Scanner for CVE-2021-3918" } + }, + { + "id": "applic_testCve4", + "fullDescription": { + "text": "The scanner checks whether the vulnerable function `ansi-regex` is called.", + "markdown": "The scanner checks whether the vulnerable function `ansi-regex` is called." + }, + "name": "CVE-2021-3807", + "shortDescription": { + "text": "Scanner for CVE-2021-3807" + } + }, + { + "id": "applic_testCve5", + "fullDescription": { + "text": "The scanner checks whether any of the following vulnerable functions are called:\n\n* `json-schema.validate` with external input to its 1st (`instance`) argument.\n* `json-schema.checkPropertyChange` with external input to its 2nd (`schema`) argument.", + "markdown": "The scanner checks whether any of the following vulnerable functions are called:\n\n* `json-schema.validate` with external input to its 1st (`instance`) argument.\n* `json-schema.checkPropertyChange` with external input to its 2nd (`schema`) argument." + }, + "name": "CVE-2021-3918", + "shortDescription": { + "text": "Scanner for CVE-2021-3918" + } + }, + { + "id": "applic_testCve1", + "fullDescription": { + "text": "The scanner checks whether the vulnerable function `ansi-regex` is called.", + "markdown": "The scanner checks whether the vulnerable function `ansi-regex` is called." + }, + "name": "CVE-2021-3807", + "shortDescription": { + "text": "Scanner for CVE-2021-3807" + } } ], "version": "APPLIC_SCANNERv0.2.3" diff --git a/xray/commands/testdata/sast-scan/contains-sast-violations.sarif b/xray/commands/testdata/sast-scan/contains-sast-violations.sarif new file mode 100644 index 000000000..d8b3c02e4 --- /dev/null +++ b/xray/commands/testdata/sast-scan/contains-sast-violations.sarif @@ -0,0 +1,907 @@ +{ + "runs": [ + { + "tool": { + "driver": { + "name": "USAF", + "rules": [ + { + "id": "python-command-injection", + "defaultConfiguration": { + "parameters": { + "properties": { + "CWE": "78" + } + } + }, + "fullDescription": { + "text": "\nRemote Code Execution is a type of vulnerability that allows an attacker\nto execute arbitrary code on a remote computer or device.\nThis can allow the attacker to gain full control of the target system, potentially\nleading to sensitive information being compromised or unauthorized actions being performed.\n\nIn this query we look for user inputs that can flow directly into execution commands\nin Python. There are many types of Command Injection, so in the future we will make\nfine-tuning changes that will need to apply to this query in the future.\n", + "markdown": "\nRemote Code Execution is a type of vulnerability that allows an attacker\nto execute arbitrary code on a remote computer or device.\nThis can allow the attacker to gain full control of the target system, potentially\nleading to sensitive information being compromised or unauthorized actions being performed.\n\nIn this query we look for user inputs that can flow directly into execution commands\nin Python. There are many types of Command Injection, so in the future we will make\nfine-tuning changes that will need to apply to this query in the future.\n" + }, + "shortDescription": { + "text": "Command Injection" + } + }, + { + "id": "python-flask-debug", + "defaultConfiguration": { + "parameters": { + "properties": { + "CWE": "1295" + } + } + }, + "fullDescription": { + "text": "\n### Overview\nDebug mode in a Flask app is a feature that allows the developer to see detailed\nerror messages and tracebacks when an error occurs. This can be useful for debugging\nand troubleshooting, but it can also create a security vulnerability if the app is\ndeployed in debug mode. In debug mode, Flask will display detailed error messages and\ntracebacks to the user, even if the error is caused by malicious input.\nThis can provide attackers with valuable information about the app's internal workings\nand vulnerabilities, making it easier for them to exploit those vulnerabilities.\n\n### Query operation\nIn this query we look Flask applications that set the `debug` argument to `True`\n\n### Vulnerable example\n```python\nfrom flask import Flask\n\napp = Flask(__name__)\n\n@app.route('/')\ndef hello():\n return 'Hello, World!'\n\nif __name__ == '__main__':\n app.run(debug=True)\n```\nIn this example, the Flask application is set to run in debug mode by passing\n`debug=True` as an argument to the `app.run()` function. This will make the application\nemit potentially sensitive information to the users.\n\n### Remediation\nWhen using `app.run`, omit the `debug` flag or set it to `False` -\n```diff\nif __name__ == '__main__':\n- app.run(debug=True)\n+ app.run()\n```\n", + "markdown": "\n### Overview\nDebug mode in a Flask app is a feature that allows the developer to see detailed\nerror messages and tracebacks when an error occurs. This can be useful for debugging\nand troubleshooting, but it can also create a security vulnerability if the app is\ndeployed in debug mode. In debug mode, Flask will display detailed error messages and\ntracebacks to the user, even if the error is caused by malicious input.\nThis can provide attackers with valuable information about the app's internal workings\nand vulnerabilities, making it easier for them to exploit those vulnerabilities.\n\n### Query operation\nIn this query we look Flask applications that set the `debug` argument to `True`\n\n### Vulnerable example\n```python\nfrom flask import Flask\n\napp = Flask(__name__)\n\n@app.route('/')\ndef hello():\n return 'Hello, World!'\n\nif __name__ == '__main__':\n app.run(debug=True)\n```\nIn this example, the Flask application is set to run in debug mode by passing\n`debug=True` as an argument to the `app.run()` function. This will make the application\nemit potentially sensitive information to the users.\n\n### Remediation\nWhen using `app.run`, omit the `debug` flag or set it to `False` -\n```diff\nif __name__ == '__main__':\n- app.run(debug=True)\n+ app.run()\n```\n" + }, + "shortDescription": { + "text": "Flask Running in Debug" + } + }, + { + "id": "python-open-redirect", + "defaultConfiguration": { + "parameters": { + "properties": { + "CWE": "601" + } + } + }, + "fullDescription": { + "text": "\n### Overview\nAn open redirect is a type of vulnerability that occurs when a web application or website\nredirects a user to an arbitrary URL, without properly validating the destination URL.\nThis can allow an attacker to redirect a user to a malicious website via a trusted website,\npotentially tricking the user into providing sensitive information or downloading malware.\n\n### Query operation\nIn this query we look for redirections that are affected by any user input.\n\n### Vulnerable example\nIn the following example, the application has a route `/redirect`\nthat takes a query parameter `url` and performs a redirection to that URL\nusing Flask's redirect() function.\n```python\nfrom flask import Flask, request, redirect\n\napp = Flask(__name__)\n\n@app.route('/')\ndef index():\n return \"\"\"\n

Welcome to Example App

\n Click here to visit Google.\n \"\"\"\n\n@app.route('/redirect')\ndef redirect_to_external():\n url = request.args.get('url', '/')\n return redirect(url)\n\nif __name__ == '__main__':\n app.run()\n```\nThe vulnerability lies in the fact that the application does not validate or sanitize the\n`url` parameter, allowing an attacker to redirect users to malicious or unintended websites.\nAn attacker could exploit this vulnerability by modifying the `url` parameter to a different\nsite, such as:\n`http://localhost:5000/redirect?url=https://www.malicious.com`\n\n### Remediation\nBefore redirection, check whether the target URL leads to a trusted domain, for example by\nusing a whitelist -\n```python\ndef is_safe_url(url):\n # Whitelist trusted domains\n trusted_domains = ['https://www.google.com', 'https://www.example.com']\n\n # Check if the provided URL is in the trusted domains\n for domain in trusted_domains:\n if url.startswith(domain):\n return True\n\n return False\n```\n\n```diff\n@app.route('/redirect')\ndef redirect_to_external():\n url = request.args.get('url', '/')\n\n # Validate the URL to ensure it's a trusted destination\n+ if is_safe_url(url):\n+ return redirect(url)\n+ else:\n+ abort(400) # Bad Request\n```\n", + "markdown": "\n### Overview\nAn open redirect is a type of vulnerability that occurs when a web application or website\nredirects a user to an arbitrary URL, without properly validating the destination URL.\nThis can allow an attacker to redirect a user to a malicious website via a trusted website,\npotentially tricking the user into providing sensitive information or downloading malware.\n\n### Query operation\nIn this query we look for redirections that are affected by any user input.\n\n### Vulnerable example\nIn the following example, the application has a route `/redirect`\nthat takes a query parameter `url` and performs a redirection to that URL\nusing Flask's redirect() function.\n```python\nfrom flask import Flask, request, redirect\n\napp = Flask(__name__)\n\n@app.route('/')\ndef index():\n return \"\"\"\n

Welcome to Example App

\n Click here to visit Google.\n \"\"\"\n\n@app.route('/redirect')\ndef redirect_to_external():\n url = request.args.get('url', '/')\n return redirect(url)\n\nif __name__ == '__main__':\n app.run()\n```\nThe vulnerability lies in the fact that the application does not validate or sanitize the\n`url` parameter, allowing an attacker to redirect users to malicious or unintended websites.\nAn attacker could exploit this vulnerability by modifying the `url` parameter to a different\nsite, such as:\n`http://localhost:5000/redirect?url=https://www.malicious.com`\n\n### Remediation\nBefore redirection, check whether the target URL leads to a trusted domain, for example by\nusing a whitelist -\n```python\ndef is_safe_url(url):\n # Whitelist trusted domains\n trusted_domains = ['https://www.google.com', 'https://www.example.com']\n\n # Check if the provided URL is in the trusted domains\n for domain in trusted_domains:\n if url.startswith(domain):\n return True\n\n return False\n```\n\n```diff\n@app.route('/redirect')\ndef redirect_to_external():\n url = request.args.get('url', '/')\n\n # Validate the URL to ensure it's a trusted destination\n+ if is_safe_url(url):\n+ return redirect(url)\n+ else:\n+ abort(400) # Bad Request\n```\n" + }, + "shortDescription": { + "text": "Open Redirect" + } + }, + { + "id": "python-parameter-injection", + "defaultConfiguration": { + "parameters": { + "properties": { + "CWE": "74" + } + } + }, + "fullDescription": { + "text": "\nRemote Code Execution is a type of vulnerability that allows an attacker\nto execute arbitrary code on a remote computer or device.\nThis can allow the attacker to gain full control of the target system, potentially\nleading to sensitive information being compromised or unauthorized actions being performed.\n\nIn this query we look for user inputs that can flow directly into execution commands\nin Python. There are many types of Command Injection, so in the future we will make\nfine-tuning changes that will need to apply to this query in the future.\n", + "markdown": "\nRemote Code Execution is a type of vulnerability that allows an attacker\nto execute arbitrary code on a remote computer or device.\nThis can allow the attacker to gain full control of the target system, potentially\nleading to sensitive information being compromised or unauthorized actions being performed.\n\nIn this query we look for user inputs that can flow directly into execution commands\nin Python. There are many types of Command Injection, so in the future we will make\nfine-tuning changes that will need to apply to this query in the future.\n" + }, + "shortDescription": { + "text": "Parameter Injection" + } + }, + { + "id": "python-path-traversal", + "defaultConfiguration": { + "parameters": { + "properties": { + "CWE": "22" + } + } + }, + "fullDescription": { + "text": "\n### Overview\nPath traversal, also known as directory traversal, is a type of vulnerability that allows an\nattacker to access files or directories on a computer or device that are outside of\nthe intended directory.\nAllowing arbitrary read access can allow the attacker to read sensitive files, such as\nconfiguration files or sensitive data, potentially leading data loss\nor even system compromise.\nAllowing arbitrary write access is more severe and in most cases leads to arbitrary code\nexecution, via editing important system files or sensitive data.\n\n### Query operation\nIn this query we look for user input that can flow un-sanitized as a path into file access\nfunctions\n(either read or write access)\n\n### Vulnerable example\n```python\nfrom flask import Flask, request, send_file\napp = Flask(__name__)\n\n@app.route('/files/')\ndef serve_file():\n filename = request.args.get('filename')\n basepath = 'static/files/'\n return send_file(basepath + filename)\n\nif __name__ == '__main__':\n app.run()\n```\nIn this example, the application has a route `/files/` that serves files from a directory\ncalled `static/files`. The vulnerability lies in the fact that the application does not\nproperly validate or sanitize the `filename` parameter, allowing an attacker to traverse\nbeyond the intended directory and access sensitive files on the server.\nAn attacker could exploit this vulnerability by manipulating the `filename` parameter\nand providing a relative path to access files outside of the `static/files` directory.\nFor example, they could craft a URL like this:\n`http://localhost:5000/files/?filename=../../../etc/passwd`\n\n### Remediation\nWhen possible, use inherently safe path functions such as `send_from_directory` that perform\nfilename escaping -\n```diff\n@app.route('/files/')\ndef serve_file():\n filename = request.args.get('filename')\n basepath = 'static/files/'\n- return send_file(basepath + filename)\n+ return send_from_directory(basepath, filename)\n```\nAlternatively, before accessing a potential path, check that the user's `filename` does not\nescape the intended path -\n```python\nfrom pathlib import Path\ndef is_escaping_path(basepath, userpath):\n try:\n Path(basepath).joinpath(userpath).resolve().relative_to(basepath.resolve())\n return False\n except ValueError:\n return True\n```\n\n```diff\n@app.route('/files/')\ndef serve_file():\n filename = request.args.get('filename')\n basepath = 'static/files/'\n+ if is_escaping_path(basepath, filename):\n+ abort(400) # Bad Request\n return send_file(basepath + filename)\n```\nAlternatively - use inherently safe\n", + "markdown": "\n### Overview\nPath traversal, also known as directory traversal, is a type of vulnerability that allows an\nattacker to access files or directories on a computer or device that are outside of\nthe intended directory.\nAllowing arbitrary read access can allow the attacker to read sensitive files, such as\nconfiguration files or sensitive data, potentially leading data loss\nor even system compromise.\nAllowing arbitrary write access is more severe and in most cases leads to arbitrary code\nexecution, via editing important system files or sensitive data.\n\n### Query operation\nIn this query we look for user input that can flow un-sanitized as a path into file access\nfunctions\n(either read or write access)\n\n### Vulnerable example\n```python\nfrom flask import Flask, request, send_file\napp = Flask(__name__)\n\n@app.route('/files/')\ndef serve_file():\n filename = request.args.get('filename')\n basepath = 'static/files/'\n return send_file(basepath + filename)\n\nif __name__ == '__main__':\n app.run()\n```\nIn this example, the application has a route `/files/` that serves files from a directory\ncalled `static/files`. The vulnerability lies in the fact that the application does not\nproperly validate or sanitize the `filename` parameter, allowing an attacker to traverse\nbeyond the intended directory and access sensitive files on the server.\nAn attacker could exploit this vulnerability by manipulating the `filename` parameter\nand providing a relative path to access files outside of the `static/files` directory.\nFor example, they could craft a URL like this:\n`http://localhost:5000/files/?filename=../../../etc/passwd`\n\n### Remediation\nWhen possible, use inherently safe path functions such as `send_from_directory` that perform\nfilename escaping -\n```diff\n@app.route('/files/')\ndef serve_file():\n filename = request.args.get('filename')\n basepath = 'static/files/'\n- return send_file(basepath + filename)\n+ return send_from_directory(basepath, filename)\n```\nAlternatively, before accessing a potential path, check that the user's `filename` does not\nescape the intended path -\n```python\nfrom pathlib import Path\ndef is_escaping_path(basepath, userpath):\n try:\n Path(basepath).joinpath(userpath).resolve().relative_to(basepath.resolve())\n return False\n except ValueError:\n return True\n```\n\n```diff\n@app.route('/files/')\ndef serve_file():\n filename = request.args.get('filename')\n basepath = 'static/files/'\n+ if is_escaping_path(basepath, filename):\n+ abort(400) # Bad Request\n return send_file(basepath + filename)\n```\nAlternatively - use inherently safe\n" + }, + "shortDescription": { + "text": "Path Traversal" + } + }, + { + "id": "python-sqli", + "defaultConfiguration": { + "parameters": { + "properties": { + "CWE": "89" + } + } + }, + "fullDescription": { + "text": "\n### Overview\nSQL injection is a type of vulnerability that allows an attacker to execute arbitrary SQL\ncommands on a database.\nThis can allow the attacker to gain access to sensitive information,\nsuch as user credentials or sensitive data, or to perform unauthorized actions,\nsuch as deleting or modifying data.\n\n### Query operation\nIn this query we check if a user input can flow un-sanitized into an SQL query.\n\n### Vulnerable example\n```python\nfrom flask import Flask, request\nimport sqlite3\n\napp = Flask(__name__)\n\n@app.route('/login', methods=['POST'])\ndef login():\n username = request.form.get('username')\n password = request.form.get('password')\n\n # Vulnerable SQL query\n query = f\"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'\"\n\n # Execute the query\n conn = sqlite3.connect('database.db')\n cursor = conn.cursor()\n cursor.execute(query)\n user = cursor.fetchone()\n conn.close()\n\n if user:\n return 'Login successful'\n else:\n return 'Login failed'\n\nif __name__ == '__main__':\n app.run()\n```\nIn this example, the application accepts a `username` and `password` from a login form via a\nPOST request. The SQL query is constructed using string concatenation, which makes it\nvulnerable to SQL injection attacks.\n\nAn attacker can exploit this vulnerability by entering `' OR 1=1 --` as the `username`.\nThe resulting query would become -\n```sql\nSELECT * FROM users WHERE username = '' OR 1=1 --' AND password = ''\n```\nwhich will always evaluate to TRUE, leading to an authentication bypass\nsince the attacker has no valid credentials.\n\n### Remediation\nReplace the vulnerable string concatenation with a parameterized query\nusing `?` placeholders -\n```diff\n@app.route('/login', methods=['POST'])\n def login():\n username = request.form.get('username')\n password = request.form.get('password')\n\n # Vulnerable SQL query\n- query = f\"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'\"\n+ query = \"SELECT * FROM users WHERE username = ? AND password = ?\"\n\n # Execute the query\n conn = sqlite3.connect('database.db')\n cursor = conn.cursor()\n- cursor.execute(query)\n+ cursor.execute(query, (username, password))\n user = cursor.fetchone()\n conn.close()\n\n if user:\n return 'Login successful'\n else:\n return 'Login failed'\n```\n", + "markdown": "\n### Overview\nSQL injection is a type of vulnerability that allows an attacker to execute arbitrary SQL\ncommands on a database.\nThis can allow the attacker to gain access to sensitive information,\nsuch as user credentials or sensitive data, or to perform unauthorized actions,\nsuch as deleting or modifying data.\n\n### Query operation\nIn this query we check if a user input can flow un-sanitized into an SQL query.\n\n### Vulnerable example\n```python\nfrom flask import Flask, request\nimport sqlite3\n\napp = Flask(__name__)\n\n@app.route('/login', methods=['POST'])\ndef login():\n username = request.form.get('username')\n password = request.form.get('password')\n\n # Vulnerable SQL query\n query = f\"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'\"\n\n # Execute the query\n conn = sqlite3.connect('database.db')\n cursor = conn.cursor()\n cursor.execute(query)\n user = cursor.fetchone()\n conn.close()\n\n if user:\n return 'Login successful'\n else:\n return 'Login failed'\n\nif __name__ == '__main__':\n app.run()\n```\nIn this example, the application accepts a `username` and `password` from a login form via a\nPOST request. The SQL query is constructed using string concatenation, which makes it\nvulnerable to SQL injection attacks.\n\nAn attacker can exploit this vulnerability by entering `' OR 1=1 --` as the `username`.\nThe resulting query would become -\n```sql\nSELECT * FROM users WHERE username = '' OR 1=1 --' AND password = ''\n```\nwhich will always evaluate to TRUE, leading to an authentication bypass\nsince the attacker has no valid credentials.\n\n### Remediation\nReplace the vulnerable string concatenation with a parameterized query\nusing `?` placeholders -\n```diff\n@app.route('/login', methods=['POST'])\n def login():\n username = request.form.get('username')\n password = request.form.get('password')\n\n # Vulnerable SQL query\n- query = f\"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'\"\n+ query = \"SELECT * FROM users WHERE username = ? AND password = ?\"\n\n # Execute the query\n conn = sqlite3.connect('database.db')\n cursor = conn.cursor()\n- cursor.execute(query)\n+ cursor.execute(query, (username, password))\n user = cursor.fetchone()\n conn.close()\n\n if user:\n return 'Login successful'\n else:\n return 'Login failed'\n```\n" + }, + "shortDescription": { + "text": "SQL Injection" + } + }, + { + "id": "python-stack-trace-exposure", + "defaultConfiguration": { + "parameters": { + "properties": { + "CWE": "209" + } + } + }, + "fullDescription": { + "text": "\n### Overview\nStack trace exposure is a type of security vulnerability that occurs when a program reveals\nsensitive information, such as the names and locations of internal files and variables,\nin error messages or other diagnostic output. This can happen when a program crashes or\nencounters an error, and the stack trace (a record of the program's call stack at the time\nof the error) is included in the output. Stack trace exposure can provide attackers with\nvaluable information about a program's internal workings and vulnerabilities, making it\neasier for them to exploit those vulnerabilities and gain unauthorized access\nto the system.\n\n### Query operation\nIn this query we look for any stack trace information flowing into the output.\n\n### Vulnerable example\n```python\nimport traceback\n\ndef my_function():\n try:\n # Some code that may raise an exception\n raise ValueError('Something went wrong')\n except ValueError as e:\n traceback.print_tb(e.__traceback__)\n\nmy_function()\n```\nIn this example, the `my_function()` function intentionally raises\na `ValueError` exception.\nThe `traceback.print_tb()` function is then used to print the stack trace\nwhen the exception is caught. The vulnerability lies in using `traceback.print_tb()`\nto output the stack trace directly to the console or any other output stream.\nIf this code were part of a web application or exposed through an API,\nthe stack trace would be exposed in the server logs or potentially returned\nas part of an error response to the client.\n\n### Remediation\nLog the exception to a logging framework or file, instead of outputting directly to the\nconsole-\n\n```python\ndef log_exception(exception):\n logging.exception('An exception occurred', exc_info=exception)\n```\n\n```diff\ndef my_function():\n try:\n # Some code that may raise an exception\n raise ValueError('Something went wrong')\n except ValueError as e:\n- traceback.print_tb(e.__traceback__)\n+ log_exception(e)\n```\n", + "markdown": "\n### Overview\nStack trace exposure is a type of security vulnerability that occurs when a program reveals\nsensitive information, such as the names and locations of internal files and variables,\nin error messages or other diagnostic output. This can happen when a program crashes or\nencounters an error, and the stack trace (a record of the program's call stack at the time\nof the error) is included in the output. Stack trace exposure can provide attackers with\nvaluable information about a program's internal workings and vulnerabilities, making it\neasier for them to exploit those vulnerabilities and gain unauthorized access\nto the system.\n\n### Query operation\nIn this query we look for any stack trace information flowing into the output.\n\n### Vulnerable example\n```python\nimport traceback\n\ndef my_function():\n try:\n # Some code that may raise an exception\n raise ValueError('Something went wrong')\n except ValueError as e:\n traceback.print_tb(e.__traceback__)\n\nmy_function()\n```\nIn this example, the `my_function()` function intentionally raises\na `ValueError` exception.\nThe `traceback.print_tb()` function is then used to print the stack trace\nwhen the exception is caught. The vulnerability lies in using `traceback.print_tb()`\nto output the stack trace directly to the console or any other output stream.\nIf this code were part of a web application or exposed through an API,\nthe stack trace would be exposed in the server logs or potentially returned\nas part of an error response to the client.\n\n### Remediation\nLog the exception to a logging framework or file, instead of outputting directly to the\nconsole-\n\n```python\ndef log_exception(exception):\n logging.exception('An exception occurred', exc_info=exception)\n```\n\n```diff\ndef my_function():\n try:\n # Some code that may raise an exception\n raise ValueError('Something went wrong')\n except ValueError as e:\n- traceback.print_tb(e.__traceback__)\n+ log_exception(e)\n```\n" + }, + "shortDescription": { + "text": "Stack Trace Exposure" + } + }, + { + "id": "python-unsafe-deserialization", + "defaultConfiguration": { + "parameters": { + "properties": { + "CWE": "502" + } + } + }, + "fullDescription": { + "text": "\n### Overview\nUnsafe deserialization is a security vulnerability that occurs when a program deserializes\nuntrusted data with a potentially dangerous deserializer.\nDeserialization is the process of converting serialized data (data that\nhas been converted into a format that can be easily transmitted or stored) back into its\noriginal form. In some (\"unsafe\") serialization protocols, if an attacker is able to\nmanipulate the serialized data, they may be able to execute arbitrary code or perform other\nmalicious actions when the data is deserialized.\n\n### Query operation\nIn this query we look for user input that can flow un-sanitized to potentially unsafe\ndeserialization methods\n\n### Vulnerable example\n```python\nimport yaml\nfrom flask import Flask, request\n\napp = Flask(__name__)\n\n@app.route('/process', methods=['POST'])\ndef process():\n data = request.get_data()\n\n # Vulnerable deserialization\n obj = yaml.load(data)\n\n # Process the deserialized object (for simplicity, we're just printing it)\n print(obj)\n\n return 'Data processed'\n\nif __name__ == '__main__':\n app.run()\n```\nIn this example, the application exposes a `/process` endpoint that accepts data via a POST\nrequest. The vulnerable code uses the `yaml.load()` function\nto deserialize the received data.\nThe vulnerability lies in the fact that the `yaml` module can execute arbitrary code\nduring the deserialization process.\nAn attacker can exploit this by crafting a malicious payload\nthat executes arbitrary code when the `yaml.load()` function is called.\n\n### Remediation\nUse deserialization routines that are known to handle untrusted data securely, such as\n`yaml.safe_load`. It is highly recommended to use the `json` module for serialization, as it\ndeserializes untrusted data securely.\n\n```diff\n@app.route('/process', methods=['POST'])\ndef process():\n data = request.get_data()\n\n # Safe deserialization\n- obj = yaml.load(data)\n+ obj = yaml.safe_load(data)\n\n # Process the deserialized object (for simplicity, we're just printing it)\n print(obj)\n\n return 'Data processed'\n```\n", + "markdown": "\n### Overview\nUnsafe deserialization is a security vulnerability that occurs when a program deserializes\nuntrusted data with a potentially dangerous deserializer.\nDeserialization is the process of converting serialized data (data that\nhas been converted into a format that can be easily transmitted or stored) back into its\noriginal form. In some (\"unsafe\") serialization protocols, if an attacker is able to\nmanipulate the serialized data, they may be able to execute arbitrary code or perform other\nmalicious actions when the data is deserialized.\n\n### Query operation\nIn this query we look for user input that can flow un-sanitized to potentially unsafe\ndeserialization methods\n\n### Vulnerable example\n```python\nimport yaml\nfrom flask import Flask, request\n\napp = Flask(__name__)\n\n@app.route('/process', methods=['POST'])\ndef process():\n data = request.get_data()\n\n # Vulnerable deserialization\n obj = yaml.load(data)\n\n # Process the deserialized object (for simplicity, we're just printing it)\n print(obj)\n\n return 'Data processed'\n\nif __name__ == '__main__':\n app.run()\n```\nIn this example, the application exposes a `/process` endpoint that accepts data via a POST\nrequest. The vulnerable code uses the `yaml.load()` function\nto deserialize the received data.\nThe vulnerability lies in the fact that the `yaml` module can execute arbitrary code\nduring the deserialization process.\nAn attacker can exploit this by crafting a malicious payload\nthat executes arbitrary code when the `yaml.load()` function is called.\n\n### Remediation\nUse deserialization routines that are known to handle untrusted data securely, such as\n`yaml.safe_load`. It is highly recommended to use the `json` module for serialization, as it\ndeserializes untrusted data securely.\n\n```diff\n@app.route('/process', methods=['POST'])\ndef process():\n data = request.get_data()\n\n # Safe deserialization\n- obj = yaml.load(data)\n+ obj = yaml.safe_load(data)\n\n # Process the deserialized object (for simplicity, we're just printing it)\n print(obj)\n\n return 'Data processed'\n```\n" + }, + "shortDescription": { + "text": "Unsafe Deserialization" + } + }, + { + "id": "python-xss", + "defaultConfiguration": { + "parameters": { + "properties": { + "CWE": "79" + } + } + }, + "fullDescription": { + "text": "\n### Overview\nXSS, or Cross-Site Scripting, is a type of vulnerability that allows an attacker to\ninject malicious code into a website or web application.\nThis can allow the attacker to steal sensitive information from users, such as their\ncookies or login credentials, or to perform unauthorized actions on their behalf.\n\n### Query operation\nIn the query we look for any user input that flows into\na potential output of the application.\n\n### Vulnerable example\nIn the following example, the Flask application takes a user-supplied parameter (`name`)\nfrom the query string and renders it directly into an HTML template using the\n`render_template_string` function. The issue is that\nthe user input is not properly sanitized or escaped, making it vulnerable to XSS attacks.\n```python\nfrom flask import Flask, request, render_template_string\n\napp = Flask(__name__)\n\n@app.route('/')\ndef index():\n name = request.args.get('name', 'Guest')\n message = f'Hello, {name}!'\n return render_template_string('

{}

'.format(message))\n\nif __name__ == '__main__':\napp.run()\n```\nAn attacker can exploit this vulnerability by injecting malicious JavaScript code into the\n`name` parameter. For instance, they could modify the URL to include the following payload:\n`http://localhost:5000/?name=`\n\n### Remediation\nWhen rendering templates, use parametrized variable assignments (which are automatically\nescaped) instead of direct string manipulation -\n```diff\n@app.route('/')\ndef index():\n name = request.args.get('name', 'Guest')\n message = f'Hello, {name}!'\n- return render_template_string('

{}

'.format(message))\n+ return render_template_string('

{{ message }}

', message=message)\n```\n", + "markdown": "\n### Overview\nXSS, or Cross-Site Scripting, is a type of vulnerability that allows an attacker to\ninject malicious code into a website or web application.\nThis can allow the attacker to steal sensitive information from users, such as their\ncookies or login credentials, or to perform unauthorized actions on their behalf.\n\n### Query operation\nIn the query we look for any user input that flows into\na potential output of the application.\n\n### Vulnerable example\nIn the following example, the Flask application takes a user-supplied parameter (`name`)\nfrom the query string and renders it directly into an HTML template using the\n`render_template_string` function. The issue is that\nthe user input is not properly sanitized or escaped, making it vulnerable to XSS attacks.\n```python\nfrom flask import Flask, request, render_template_string\n\napp = Flask(__name__)\n\n@app.route('/')\ndef index():\n name = request.args.get('name', 'Guest')\n message = f'Hello, {name}!'\n return render_template_string('

{}

'.format(message))\n\nif __name__ == '__main__':\napp.run()\n```\nAn attacker can exploit this vulnerability by injecting malicious JavaScript code into the\n`name` parameter. For instance, they could modify the URL to include the following payload:\n`http://localhost:5000/?name=`\n\n### Remediation\nWhen rendering templates, use parametrized variable assignments (which are automatically\nescaped) instead of direct string manipulation -\n```diff\n@app.route('/')\ndef index():\n name = request.args.get('name', 'Guest')\n message = f'Hello, {name}!'\n- return render_template_string('

{}

'.format(message))\n+ return render_template_string('

{{ message }}

', message=message)\n```\n" + }, + "shortDescription": { + "text": "XSS Vulnerability" + } + } + ] + } + }, + "invocations": [ + { + "executionSuccessful": true, + "arguments": [ + "/Users/assafa/.jfrog/dependencies/analyzerManager/zd_scanner/scanner", + "scan", + "/var/folders/xv/th4cksxn7jv9wjrdnn1h4tj00000gq/T/jfrog.cli.temp.-1693477603-3697552683/results.sarif" + ], + "workingDirectory": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat" + } + } + ], + "results": [ + { + "message": { + "text": "SQL Injection" + }, + "codeFlows": [ + { + "threadFlows": [ + { + "locations": [ + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 28, + "endLine": 9, + "snippet": { + "text": "request.form" + }, + "startColumn": 16, + "startLine": 9 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 32, + "endLine": 9, + "snippet": { + "text": "request.form.get" + }, + "startColumn": 16, + "startLine": 9 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 44, + "endLine": 9, + "snippet": { + "text": "request.form.get(\"username\")" + }, + "startColumn": 16, + "startLine": 9 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 13, + "endLine": 9, + "snippet": { + "text": "username" + }, + "startColumn": 5, + "startLine": 9 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 31, + "endLine": 20, + "snippet": { + "text": "(username, password)" + }, + "startColumn": 11, + "startLine": 20 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 31, + "endLine": 20, + "snippet": { + "text": "\"SELECT id, username, access_level FROM user WHERE username = '%s' AND password = '%s'\"\n % (username, password)" + }, + "startColumn": 9, + "startLine": 19 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 10, + "endLine": 18, + "snippet": { + "text": "query" + }, + "startColumn": 5, + "startLine": 18 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 39, + "endLine": 22, + "snippet": { + "text": "query_db(query, [], True)" + }, + "startColumn": 14, + "startLine": 22 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.__init__.query_db" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/__init__.py" + }, + "region": { + "endColumn": 19, + "endLine": 10, + "snippet": { + "text": "query" + }, + "startColumn": 14, + "startLine": 10 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.__init__.query_db" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/__init__.py" + }, + "region": { + "endColumn": 49, + "endLine": 14, + "snippet": { + "text": "conn.cursor().execute(query, args)" + }, + "startColumn": 15, + "startLine": 14 + } + } + } + } + ] + } + ] + } + ], + "level": "error", + "locations": [ + { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.__init__.query_db" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/__init__.py" + }, + "region": { + "endColumn": 49, + "endLine": 14, + "snippet": { + "text": "conn.cursor().execute(query, args)" + }, + "startColumn": 15, + "startLine": 14 + } + } + } + ], + "ruleId": "python-sqli" + }, + { + "message": { + "text": "SQL Injection" + }, + "codeFlows": [ + { + "threadFlows": [ + { + "locations": [ + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 28, + "endLine": 10, + "snippet": { + "text": "request.form" + }, + "startColumn": 16, + "startLine": 10 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 32, + "endLine": 10, + "snippet": { + "text": "request.form.get" + }, + "startColumn": 16, + "startLine": 10 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 44, + "endLine": 10, + "snippet": { + "text": "request.form.get(\"password\")" + }, + "startColumn": 16, + "startLine": 10 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 13, + "endLine": 10, + "snippet": { + "text": "password" + }, + "startColumn": 5, + "startLine": 10 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 31, + "endLine": 20, + "snippet": { + "text": "(username, password)" + }, + "startColumn": 11, + "startLine": 20 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 31, + "endLine": 20, + "snippet": { + "text": "\"SELECT id, username, access_level FROM user WHERE username = '%s' AND password = '%s'\"\n % (username, password)" + }, + "startColumn": 9, + "startLine": 19 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 10, + "endLine": 18, + "snippet": { + "text": "query" + }, + "startColumn": 5, + "startLine": 18 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 39, + "endLine": 22, + "snippet": { + "text": "query_db(query, [], True)" + }, + "startColumn": 14, + "startLine": 22 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.__init__.query_db" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/__init__.py" + }, + "region": { + "endColumn": 19, + "endLine": 10, + "snippet": { + "text": "query" + }, + "startColumn": 14, + "startLine": 10 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.__init__.query_db" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/__init__.py" + }, + "region": { + "endColumn": 49, + "endLine": 14, + "snippet": { + "text": "conn.cursor().execute(query, args)" + }, + "startColumn": 15, + "startLine": 14 + } + } + } + } + ] + } + ] + } + ], + "level": "error", + "locations": [ + { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.__init__.query_db" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/__init__.py" + }, + "region": { + "endColumn": 49, + "endLine": 14, + "snippet": { + "text": "conn.cursor().execute(query, args)" + }, + "startColumn": 15, + "startLine": 14 + } + } + } + ], + "ruleId": "python-sqli" + }, + { + "message": { + "text": "Open Redirect" + }, + "codeFlows": [ + { + "threadFlows": [ + { + "locations": [ + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login_and_redirect" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 23, + "endLine": 33, + "snippet": { + "text": "request.args" + }, + "startColumn": 11, + "startLine": 33 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login_and_redirect" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 27, + "endLine": 33, + "snippet": { + "text": "request.args.get" + }, + "startColumn": 11, + "startLine": 33 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login_and_redirect" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 34, + "endLine": 33, + "snippet": { + "text": "request.args.get(\"url\")" + }, + "startColumn": 11, + "startLine": 33 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login_and_redirect" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 8, + "endLine": 33, + "snippet": { + "text": "url" + }, + "startColumn": 5, + "startLine": 33 + } + } + } + }, + { + "location": { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login_and_redirect" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 29, + "endLine": 46, + "snippet": { + "text": "redirect(url)" + }, + "startColumn": 16, + "startLine": 46 + } + } + } + } + ] + } + ] + } + ], + "level": "note", + "locations": [ + { + "logicalLocations": [ + { + "fullyQualifiedName": "flask_webgoat.auth.login_and_redirect" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/flask_webgoat/auth.py" + }, + "region": { + "endColumn": 29, + "endLine": 46, + "snippet": { + "text": "redirect(url)" + }, + "startColumn": 16, + "startLine": 46 + } + } + } + ], + "ruleId": "python-open-redirect" + }, + { + "message": { + "text": "Flask Running in Debug" + }, + "locations": [ + { + "logicalLocations": [ + { + "fullyQualifiedName": "run" + } + ], + "physicalLocation": { + "artifactLocation": { + "uri": "file:///Users/assafa/Documents/code/flask-webgoat/run.py" + }, + "region": { + "endColumn": 24, + "endLine": 15, + "snippet": { + "text": "app.run(debug=True)" + }, + "startColumn": 5, + "startLine": 15 + } + } + } + ], + "ruleId": "python-flask-debug" + } + ] + } + ], + "version": "2.1.0", + "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/cos02/schemas/sarif-schema-2.1.0.json" +} \ No newline at end of file diff --git a/xray/commands/testdata/sast-scan/no-violations.sarif b/xray/commands/testdata/sast-scan/no-violations.sarif new file mode 100644 index 000000000..ed129e6e0 --- /dev/null +++ b/xray/commands/testdata/sast-scan/no-violations.sarif @@ -0,0 +1,28 @@ +{ + "runs": [ + { + "tool": { + "driver": { + "name": "USAF", + "rules": [] + } + }, + "invocations": [ + { + "executionSuccessful": true, + "arguments": [ + "/Users/assafa/.jfrog/dependencies/analyzerManager/zd_scanner/scanner", + "scan", + "/var/folders/xv/th4cksxn7jv9wjrdnn1h4tj00000gq/T/jfrog.cli.temp.-1693477603-3697552683/results.sarif" + ], + "workingDirectory": { + "uri": "file:///Users/assafa/Documents/code/terraform" + } + } + ], + "results": [] + } + ], + "version": "2.1.0", + "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/cos02/schemas/sarif-schema-2.1.0.json" +} \ No newline at end of file diff --git a/xray/commands/utils/utils.go b/xray/commands/utils/utils.go deleted file mode 100644 index b3a4ccc2d..000000000 --- a/xray/commands/utils/utils.go +++ /dev/null @@ -1,213 +0,0 @@ -package utils - -import ( - "fmt" - "github.com/jfrog/jfrog-cli-core/v2/utils/config" - "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" - "github.com/jfrog/jfrog-cli-core/v2/xray/utils" - clientconfig "github.com/jfrog/jfrog-client-go/config" - clientutils "github.com/jfrog/jfrog-client-go/utils" - "github.com/jfrog/jfrog-client-go/utils/errorutils" - "github.com/jfrog/jfrog-client-go/utils/log" - "github.com/jfrog/jfrog-client-go/xray" - "github.com/jfrog/jfrog-client-go/xray/services" - "golang.org/x/text/cases" - "golang.org/x/text/language" - "os" - "strings" -) - -const ( - GraphScanMinXrayVersion = "3.29.0" - ScanTypeMinXrayVersion = "3.37.2" - BypassArchiveLimitsMinXrayVersion = "3.59.0" - TotalConcurrentRequests = 10 -) - -func getLevelOfSeverity(s string) int { - severity := utils.GetSeverity(cases.Title(language.Und).String(s), utils.ApplicabilityUndeterminedStringValue) - return severity.NumValue() -} - -type ScanGraphParams struct { - serverDetails *config.ServerDetails - xrayGraphScanParams *services.XrayGraphScanParams - fixableOnly bool - xrayVersion string - severityLevel int -} - -func NewScanGraphParams() *ScanGraphParams { - return &ScanGraphParams{} -} - -func (sgp *ScanGraphParams) SetServerDetails(serverDetails *config.ServerDetails) *ScanGraphParams { - sgp.serverDetails = serverDetails - return sgp -} - -func (sgp *ScanGraphParams) SetXrayGraphScanParams(params *services.XrayGraphScanParams) *ScanGraphParams { - sgp.xrayGraphScanParams = params - return sgp -} - -func (sgp *ScanGraphParams) SetXrayVersion(xrayVersion string) *ScanGraphParams { - sgp.xrayVersion = xrayVersion - return sgp -} - -func (sgp *ScanGraphParams) SetSeverityLevel(severity string) *ScanGraphParams { - sgp.severityLevel = getLevelOfSeverity(severity) - return sgp -} - -func (sgp *ScanGraphParams) XrayGraphScanParams() *services.XrayGraphScanParams { - return sgp.xrayGraphScanParams -} - -func (sgp *ScanGraphParams) XrayVersion() string { - return sgp.xrayVersion -} - -func (sgp *ScanGraphParams) ServerDetails() *config.ServerDetails { - return sgp.serverDetails -} - -func (sgp *ScanGraphParams) FixableOnly() bool { - return sgp.fixableOnly -} - -func (sgp *ScanGraphParams) SetFixableOnly(fixable bool) *ScanGraphParams { - sgp.fixableOnly = fixable - return sgp -} - -func CreateXrayServiceManager(serviceDetails *config.ServerDetails) (*xray.XrayServicesManager, error) { - xrayDetails, err := serviceDetails.CreateXrayAuthConfig() - if err != nil { - return nil, err - } - serviceConfig, err := clientconfig.NewConfigBuilder(). - SetServiceDetails(xrayDetails). - Build() - if err != nil { - return nil, err - } - return xray.New(serviceConfig) -} - -func RunScanGraphAndGetResults(params *ScanGraphParams) (*services.ScanResponse, error) { - xrayManager, err := CreateXrayServiceManager(params.serverDetails) - if err != nil { - return nil, err - } - - err = clientutils.ValidateMinimumVersion(clientutils.Xray, params.xrayVersion, ScanTypeMinXrayVersion) - if err != nil { - // Remove scan type param if Xray version is under the minimum supported version - params.xrayGraphScanParams.ScanType = "" - } - scanId, err := xrayManager.ScanGraph(*params.xrayGraphScanParams) - if err != nil { - return nil, err - } - scanResult, err := xrayManager.GetScanGraphResults(scanId, params.XrayGraphScanParams().IncludeVulnerabilities, params.XrayGraphScanParams().IncludeLicenses) - if err != nil { - return nil, err - } - return filterResultIfNeeded(scanResult, params), nil -} - -func filterResultIfNeeded(scanResult *services.ScanResponse, params *ScanGraphParams) *services.ScanResponse { - if !shouldFilterResults(params) { - return scanResult - } - - scanResult.Violations = filterViolations(scanResult.Violations, params) - scanResult.Vulnerabilities = filterVulnerabilities(scanResult.Vulnerabilities, params) - return scanResult -} - -func shouldFilterResults(params *ScanGraphParams) bool { - return params.severityLevel > 0 || params.fixableOnly -} - -func filterViolations(violations []services.Violation, params *ScanGraphParams) []services.Violation { - var filteredViolations []services.Violation - for _, violation := range violations { - if params.fixableOnly { - violation.Components = getFixableComponents(violation.Components) - if len(violation.Components) == 0 { - // All the components were filtered, filter this violation - continue - } - } - if getLevelOfSeverity(violation.Severity) >= params.severityLevel { - filteredViolations = append(filteredViolations, violation) - } - } - return filteredViolations -} - -func filterVulnerabilities(vulnerabilities []services.Vulnerability, params *ScanGraphParams) []services.Vulnerability { - var filteredVulnerabilities []services.Vulnerability - for _, vulnerability := range vulnerabilities { - if params.fixableOnly { - vulnerability.Components = getFixableComponents(vulnerability.Components) - if len(vulnerability.Components) == 0 { - // All the components were filtered, filter this violation - continue - } - } - if getLevelOfSeverity(vulnerability.Severity) >= params.severityLevel { - filteredVulnerabilities = append(filteredVulnerabilities, vulnerability) - } - } - return filteredVulnerabilities -} - -func getFixableComponents(components map[string]services.Component) map[string]services.Component { - fixableComponents := make(map[string]services.Component) - for vulnKey, vulnDetails := range components { - if len(vulnDetails.FixedVersions) > 0 { - fixableComponents[vulnKey] = vulnDetails - } - } - return fixableComponents -} - -func CreateXrayServiceManagerAndGetVersion(serviceDetails *config.ServerDetails) (*xray.XrayServicesManager, string, error) { - xrayManager, err := CreateXrayServiceManager(serviceDetails) - if err != nil { - return nil, "", err - } - xrayVersion, err := xrayManager.GetVersion() - if err != nil { - return nil, "", err - } - return xrayManager, xrayVersion, nil -} - -func DetectedTechnologies() (technologies []string) { - wd, err := os.Getwd() - if errorutils.CheckError(err) != nil { - return - } - detectedTechnologies, err := coreutils.DetectTechnologies(wd, false, false) - if err != nil { - return - } - if len(detectedTechnologies) == 0 { - return - } - techStringsList := coreutils.DetectedTechnologiesToSlice(detectedTechnologies) - log.Info(fmt.Sprintf("Detected: %s.", strings.Join(techStringsList, ","))) - return techStringsList -} - -func DetectNumOfThreads(threadsCount int) (int, error) { - if threadsCount > TotalConcurrentRequests { - return 0, errorutils.CheckErrorf("number of threads crossed the maximum, the maximum threads allowed is %v", TotalConcurrentRequests) - } - return threadsCount, nil -} diff --git a/xray/formats/conversion.go b/xray/formats/conversion.go index 88e2c2612..f210ae708 100644 --- a/xray/formats/conversion.go +++ b/xray/formats/conversion.go @@ -140,25 +140,37 @@ func ConvertToOperationalRiskViolationScanTableRow(rows []OperationalRiskViolati return } -func ConvertToSecretsTableRow(rows []IacSecretsRow) (tableRows []secretsTableRow) { +func ConvertToSecretsTableRow(rows []SourceCodeRow) (tableRows []secretsTableRow) { for i := range rows { tableRows = append(tableRows, secretsTableRow{ severity: rows[i].Severity, file: rows[i].File, lineColumn: rows[i].LineColumn, - text: rows[i].Text, + text: rows[i].Snippet, }) } return } -func ConvertToIacTableRow(rows []IacSecretsRow) (tableRows []iacTableRow) { +func ConvertToIacTableRow(rows []SourceCodeRow) (tableRows []iacTableRow) { for i := range rows { tableRows = append(tableRows, iacTableRow{ severity: rows[i].Severity, file: rows[i].File, lineColumn: rows[i].LineColumn, - text: rows[i].Text, + text: rows[i].Snippet, + }) + } + return +} + +func ConvertToSastTableRow(rows []SourceCodeRow) (tableRows []sastTableRow) { + for i := range rows { + tableRows = append(tableRows, sastTableRow{ + severity: rows[i].Severity, + file: rows[i].File, + lineColumn: rows[i].LineColumn, + text: rows[i].Snippet, }) } return diff --git a/xray/formats/simplejsonapi.go b/xray/formats/simplejsonapi.go index 54cccdcf8..5a2aa1ff1 100644 --- a/xray/formats/simplejsonapi.go +++ b/xray/formats/simplejsonapi.go @@ -12,8 +12,9 @@ type SimpleJsonResults struct { LicensesViolations []LicenseViolationRow `json:"licensesViolations"` Licenses []LicenseRow `json:"licenses"` OperationalRiskViolations []OperationalRiskViolationRow `json:"operationalRiskViolations"` - Secrets []IacSecretsRow `json:"secrets"` - Iacs []IacSecretsRow `json:"iacViolations"` + Secrets []SourceCodeRow `json:"secrets"` + Iacs []SourceCodeRow `json:"iacViolations"` + Sast []SourceCodeRow `json:"sastViolations"` Errors []SimpleJsonError `json:"errors"` } @@ -73,13 +74,18 @@ type OperationalRiskViolationRow struct { LatestVersion string `json:"latestVersion"` } -type IacSecretsRow struct { +type SourceCodeRow struct { Severity string `json:"severity"` SeverityNumValue int `json:"-"` // For sorting - File string `json:"file"` - LineColumn string `json:"lineColumn"` - Text string `json:"text"` - Type string `json:"type"` + SourceCodeLocationRow + Type string `json:"type"` + CodeFlow [][]SourceCodeLocationRow `json:"codeFlow,omitempty"` +} + +type SourceCodeLocationRow struct { + File string `json:"file"` + LineColumn string `json:"lineColumn"` + Snippet string `json:"snippet"` } type ComponentRow struct { @@ -88,9 +94,21 @@ type ComponentRow struct { } type CveRow struct { - Id string `json:"id"` - CvssV2 string `json:"cvssV2"` - CvssV3 string `json:"cvssV3"` + Id string `json:"id"` + CvssV2 string `json:"cvssV2"` + CvssV3 string `json:"cvssV3"` + Applicability *Applicability `json:"applicability,omitempty"` +} + +type Applicability struct { + Status bool `json:"status"` + ScannerDescription string `json:"scannerDescription,omitempty"` + Evidence []Evidence `json:"evidence,omitempty"` +} + +type Evidence struct { + SourceCodeLocationRow + Reason string `json:"reason,omitempty"` } type SimpleJsonError struct { diff --git a/xray/formats/table.go b/xray/formats/table.go index 44fe79f20..c099b058d 100644 --- a/xray/formats/table.go +++ b/xray/formats/table.go @@ -136,3 +136,10 @@ type iacTableRow struct { lineColumn string `col-name:"Line:Column"` text string `col-name:"Finding"` } + +type sastTableRow struct { + severity string `col-name:"Severity"` + file string `col-name:"File"` + lineColumn string `col-name:"Line:Column"` + text string `col-name:"Finding"` +} diff --git a/xray/scangraph/params.go b/xray/scangraph/params.go new file mode 100644 index 000000000..76e0106e7 --- /dev/null +++ b/xray/scangraph/params.go @@ -0,0 +1,59 @@ +package scangraph + +import ( + "github.com/jfrog/jfrog-cli-core/v2/utils/config" + "github.com/jfrog/jfrog-client-go/xray/services" +) + +type ScanGraphParams struct { + serverDetails *config.ServerDetails + xrayGraphScanParams *services.XrayGraphScanParams + fixableOnly bool + xrayVersion string + severityLevel int +} + +func NewScanGraphParams() *ScanGraphParams { + return &ScanGraphParams{} +} + +func (sgp *ScanGraphParams) SetServerDetails(serverDetails *config.ServerDetails) *ScanGraphParams { + sgp.serverDetails = serverDetails + return sgp +} + +func (sgp *ScanGraphParams) SetXrayGraphScanParams(params *services.XrayGraphScanParams) *ScanGraphParams { + sgp.xrayGraphScanParams = params + return sgp +} + +func (sgp *ScanGraphParams) SetXrayVersion(xrayVersion string) *ScanGraphParams { + sgp.xrayVersion = xrayVersion + return sgp +} + +func (sgp *ScanGraphParams) SetSeverityLevel(severity string) *ScanGraphParams { + sgp.severityLevel = getLevelOfSeverity(severity) + return sgp +} + +func (sgp *ScanGraphParams) XrayGraphScanParams() *services.XrayGraphScanParams { + return sgp.xrayGraphScanParams +} + +func (sgp *ScanGraphParams) XrayVersion() string { + return sgp.xrayVersion +} + +func (sgp *ScanGraphParams) ServerDetails() *config.ServerDetails { + return sgp.serverDetails +} + +func (sgp *ScanGraphParams) FixableOnly() bool { + return sgp.fixableOnly +} + +func (sgp *ScanGraphParams) SetFixableOnly(fixable bool) *ScanGraphParams { + sgp.fixableOnly = fixable + return sgp +} diff --git a/xray/scangraph/scangraph.go b/xray/scangraph/scangraph.go new file mode 100644 index 000000000..22dc755f0 --- /dev/null +++ b/xray/scangraph/scangraph.go @@ -0,0 +1,108 @@ +package scangraph + +import ( + "github.com/jfrog/jfrog-cli-core/v2/xray/utils" + clientutils "github.com/jfrog/jfrog-client-go/utils" + "github.com/jfrog/jfrog-client-go/xray/services" + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +const ( + GraphScanMinXrayVersion = "3.29.0" + ScanTypeMinXrayVersion = "3.37.2" +) + +func RunScanGraphAndGetResults(params *ScanGraphParams) (*services.ScanResponse, error) { + xrayManager, err := utils.CreateXrayServiceManager(params.serverDetails) + if err != nil { + return nil, err + } + + err = clientutils.ValidateMinimumVersion(clientutils.Xray, params.xrayVersion, ScanTypeMinXrayVersion) + if err != nil { + // Remove scan type param if Xray version is under the minimum supported version + params.xrayGraphScanParams.ScanType = "" + } + + if params.xrayGraphScanParams.XscGitInfoContext != nil { + if params.xrayGraphScanParams.XscVersion, err = xrayManager.XscEnabled(); err != nil { + return nil, err + } + } + + scanId, err := xrayManager.ScanGraph(*params.xrayGraphScanParams) + if err != nil { + return nil, err + } + + xscEnabled := params.xrayGraphScanParams.XscVersion != "" + scanResult, err := xrayManager.GetScanGraphResults(scanId, params.XrayGraphScanParams().IncludeVulnerabilities, params.XrayGraphScanParams().IncludeLicenses, xscEnabled) + if err != nil { + return nil, err + } + return filterResultIfNeeded(scanResult, params), nil +} + +func filterResultIfNeeded(scanResult *services.ScanResponse, params *ScanGraphParams) *services.ScanResponse { + if !shouldFilterResults(params) { + return scanResult + } + + scanResult.Violations = filterViolations(scanResult.Violations, params) + scanResult.Vulnerabilities = filterVulnerabilities(scanResult.Vulnerabilities, params) + return scanResult +} + +func shouldFilterResults(params *ScanGraphParams) bool { + return params.severityLevel > 0 || params.fixableOnly +} + +func filterViolations(violations []services.Violation, params *ScanGraphParams) []services.Violation { + var filteredViolations []services.Violation + for _, violation := range violations { + if params.fixableOnly { + violation.Components = getFixableComponents(violation.Components) + if len(violation.Components) == 0 { + // All the components were filtered, filter this violation + continue + } + } + if getLevelOfSeverity(violation.Severity) >= params.severityLevel { + filteredViolations = append(filteredViolations, violation) + } + } + return filteredViolations +} + +func filterVulnerabilities(vulnerabilities []services.Vulnerability, params *ScanGraphParams) []services.Vulnerability { + var filteredVulnerabilities []services.Vulnerability + for _, vulnerability := range vulnerabilities { + if params.fixableOnly { + vulnerability.Components = getFixableComponents(vulnerability.Components) + if len(vulnerability.Components) == 0 { + // All the components were filtered, filter this violation + continue + } + } + if getLevelOfSeverity(vulnerability.Severity) >= params.severityLevel { + filteredVulnerabilities = append(filteredVulnerabilities, vulnerability) + } + } + return filteredVulnerabilities +} + +func getFixableComponents(components map[string]services.Component) map[string]services.Component { + fixableComponents := make(map[string]services.Component) + for vulnKey, vulnDetails := range components { + if len(vulnDetails.FixedVersions) > 0 { + fixableComponents[vulnKey] = vulnDetails + } + } + return fixableComponents +} + +func getLevelOfSeverity(s string) int { + severity := utils.GetSeverity(cases.Title(language.Und).String(s), utils.ApplicabilityUndetermined) + return severity.NumValue() +} diff --git a/xray/commands/utils/utils_test.go b/xray/scangraph/scangraph_test.go similarity index 98% rename from xray/commands/utils/utils_test.go rename to xray/scangraph/scangraph_test.go index 56d5bf0cc..28c5d19f6 100644 --- a/xray/commands/utils/utils_test.go +++ b/xray/scangraph/scangraph_test.go @@ -1,4 +1,4 @@ -package utils +package scangraph import ( "github.com/jfrog/jfrog-client-go/xray/services" @@ -72,7 +72,7 @@ func TestFilterResultIfNeeded(t *testing.T) { }, }, params: ScanGraphParams{ - severityLevel: 8, + severityLevel: 11, }, expected: services.ScanResponse{ Violations: []services.Violation{ diff --git a/xray/utils/analyzermanager.go b/xray/utils/analyzermanager.go index e88acb735..0f11cbb4d 100644 --- a/xray/utils/analyzermanager.go +++ b/xray/utils/analyzermanager.go @@ -3,12 +3,11 @@ package utils import ( "errors" "fmt" + "github.com/jfrog/gofrog/version" "os" "os/exec" "path" "path/filepath" - "strconv" - "strings" "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" @@ -19,46 +18,47 @@ import ( "github.com/owenrumney/go-sarif/v2/sarif" ) -var ( - levelToSeverity = map[string]string{"error": "High", "warning": "Medium", "info": "Low"} -) - const ( - EntitlementsMinVersion = "3.66.5" - ApplicabilityFeatureId = "contextual_analysis" - AnalyzerManagerZipName = "analyzerManager.zip" - analyzerManagerVersion = "1.2.4.1953469" - analyzerManagerDownloadPath = "xsc-gen-exe-analyzer-manager-local/v1" - analyzerManagerDirName = "analyzerManager" - analyzerManagerExecutableName = "analyzerManager" - analyzerManagerLogDirName = "analyzerManagerLogs" - jfUserEnvVariable = "JF_USER" - jfPasswordEnvVariable = "JF_PASS" - jfTokenEnvVariable = "JF_TOKEN" - jfPlatformUrlEnvVariable = "JF_PLATFORM_URL" - logDirEnvVariable = "AM_LOG_DIRECTORY" - SeverityDefaultValue = "Medium" - notEntitledExitCode = 31 - unsupportedCommandExitCode = 13 - unsupportedOsExitCode = 55 - ErrFailedScannerRun = "failed to run %s scan. Exit code received: %s" + EntitlementsMinVersion = "3.66.5" + ApplicabilityFeatureId = "contextual_analysis" + AnalyzerManagerZipName = "analyzerManager.zip" + defaultAnalyzerManagerVersion = "1.2.4.1953469" + minAnalyzerManagerVersionForSast = "1.3" + analyzerManagerDownloadPath = "xsc-gen-exe-analyzer-manager-local/v1" + analyzerManagerDirName = "analyzerManager" + analyzerManagerExecutableName = "analyzerManager" + analyzerManagerLogDirName = "analyzerManagerLogs" + jfUserEnvVariable = "JF_USER" + jfPasswordEnvVariable = "JF_PASS" + jfTokenEnvVariable = "JF_TOKEN" + jfPlatformUrlEnvVariable = "JF_PLATFORM_URL" + logDirEnvVariable = "AM_LOG_DIRECTORY" + notEntitledExitCode = 31 + unsupportedCommandExitCode = 13 + unsupportedOsExitCode = 55 + ErrFailedScannerRun = "failed to run %s scan. Exit code received: %s" + jfrogCliAnalyzerManagerVersionEnvVariable = "JFROG_CLI_ANALYZER_MANAGER_VERSION" ) +type ApplicabilityStatus string + const ( - ApplicableStringValue = "Applicable" - NotApplicableStringValue = "Not Applicable" - ApplicabilityUndeterminedStringValue = "Undetermined" + Applicable ApplicabilityStatus = "Applicable" + NotApplicable ApplicabilityStatus = "Not Applicable" + ApplicabilityUndetermined ApplicabilityStatus = "Undetermined" + NotScanned ApplicabilityStatus = "" ) -type ScanType string +type JasScanType string const ( - Applicability ScanType = "Applicability" - Secrets ScanType = "Secrets" - IaC ScanType = "IaC" + Applicability JasScanType = "Applicability" + Secrets JasScanType = "Secrets" + IaC JasScanType = "IaC" + Sast JasScanType = "Sast" ) -func (st ScanType) FormattedError(err error) error { +func (st JasScanType) FormattedError(err error) error { if err != nil { return fmt.Errorf(ErrFailedScannerRun, st, err.Error()) } @@ -71,20 +71,15 @@ var exitCodeErrorsMap = map[int]string{ unsupportedOsExitCode: "got unsupported operating system error from analyzer manager", } -type IacOrSecretResult struct { - Severity string - File string - LineColumn string - Type string - Text string -} - type ExtendedScanResults struct { - XrayResults []services.ScanResponse - ScannedTechnologies []coreutils.Technology - ApplicabilityScanResults map[string]string - SecretsScanResults []IacOrSecretResult - IacScanResults []IacOrSecretResult + XrayResults []services.ScanResponse + XrayVersion string + ScannedTechnologies []coreutils.Technology + + ApplicabilityScanResults []*sarif.Run + SecretsScanResults []*sarif.Run + IacScanResults []*sarif.Run + SastScanResults []*sarif.Run EntitledForJas bool } @@ -94,13 +89,14 @@ func (e *ExtendedScanResults) getXrayScanResults() []services.ScanResponse { type AnalyzerManager struct { AnalyzerManagerFullPath string + MultiScanId string } -func (am *AnalyzerManager) Exec(configFile, scanCommand string, serverDetails *config.ServerDetails) (err error) { +func (am *AnalyzerManager) Exec(configFile, scanCommand, workingDir string, serverDetails *config.ServerDetails) (err error) { if err = SetAnalyzerManagerEnvVariables(serverDetails); err != nil { return err } - cmd := exec.Command(am.AnalyzerManagerFullPath, scanCommand, configFile) + cmd := exec.Command(am.AnalyzerManagerFullPath, scanCommand, configFile, am.MultiScanId) defer func() { if !cmd.ProcessState.Exited() { if killProcessError := cmd.Process.Kill(); errorutils.CheckError(killProcessError) != nil { @@ -108,7 +104,7 @@ func (am *AnalyzerManager) Exec(configFile, scanCommand string, serverDetails *c } } }() - cmd.Dir = filepath.Dir(am.AnalyzerManagerFullPath) + cmd.Dir = workingDir err = cmd.Run() return errorutils.CheckError(err) } @@ -118,7 +114,18 @@ func GetAnalyzerManagerDownloadPath() (string, error) { if err != nil { return "", err } - return path.Join(analyzerManagerDownloadPath, analyzerManagerVersion, osAndArc, AnalyzerManagerZipName), nil + return path.Join(analyzerManagerDownloadPath, GetAnalyzerManagerVersion(), osAndArc, AnalyzerManagerZipName), nil +} + +func GetAnalyzerManagerVersion() string { + if analyzerManagerVersion, exists := os.LookupEnv(jfrogCliAnalyzerManagerVersionEnvVariable); exists { + return analyzerManagerVersion + } + return defaultAnalyzerManagerVersion +} + +func IsSastSupported() bool { + return version.NewVersion(GetAnalyzerManagerVersion()).AtLeast(minAnalyzerManagerVersionForSast) } func GetAnalyzerManagerDirAbsolutePath() (string, error) { @@ -179,7 +186,7 @@ func SetAnalyzerManagerEnvVariables(serverDetails *config.ServerDetails) error { return nil } -func ParseAnalyzerManagerError(scanner ScanType, err error) error { +func ParseAnalyzerManagerError(scanner JasScanType, err error) error { var exitError *exec.ExitError if errors.As(err, &exitError) { exitCode := exitError.ExitCode() @@ -190,72 +197,3 @@ func ParseAnalyzerManagerError(scanner ScanType, err error) error { } return scanner.FormattedError(err) } - -func RemoveDuplicateValues(stringSlice []string) []string { - keys := make(map[string]bool) - finalSlice := []string{} - for _, entry := range stringSlice { - if _, value := keys[entry]; !value { - keys[entry] = true - finalSlice = append(finalSlice, entry) - } - } - return finalSlice -} - -func GetResultFileName(result *sarif.Result) string { - if len(result.Locations) > 0 { - filePath := result.Locations[0].PhysicalLocation.ArtifactLocation.URI - if filePath != nil { - return *filePath - } - } - return "" -} - -func GetResultLocationInFile(result *sarif.Result) string { - if len(result.Locations) > 0 { - startLine := result.Locations[0].PhysicalLocation.Region.StartLine - startColumn := result.Locations[0].PhysicalLocation.Region.StartColumn - if startLine != nil && startColumn != nil { - return strconv.Itoa(*startLine) + ":" + strconv.Itoa(*startColumn) - } - } - return "" -} - -func ExtractRelativePath(resultPath string, projectRoot string) string { - filePrefix := "file://" - relativePath := strings.ReplaceAll(strings.ReplaceAll(resultPath, projectRoot, ""), filePrefix, "") - return relativePath -} - -func GetResultSeverity(result *sarif.Result) string { - if result.Level != nil { - if severity, ok := levelToSeverity[*result.Level]; ok { - return severity - } - } - return SeverityDefaultValue -} - -// Receives a list of relative path working dirs, returns a list of full paths working dirs -func GetFullPathsWorkingDirs(workingDirs []string) ([]string, error) { - if len(workingDirs) == 0 { - currentDir, err := coreutils.GetWorkingDirectory() - if err != nil { - return nil, err - } - return []string{currentDir}, nil - } - - var fullPathsWorkingDirs []string - for _, wd := range workingDirs { - fullPathWd, err := filepath.Abs(wd) - if err != nil { - return nil, err - } - fullPathsWorkingDirs = append(fullPathsWorkingDirs, fullPathWd) - } - return fullPathsWorkingDirs, nil -} diff --git a/xray/utils/analyzermanager_test.go b/xray/utils/analyzermanager_test.go index a0fedcca1..602d33686 100644 --- a/xray/utils/analyzermanager_test.go +++ b/xray/utils/analyzermanager_test.go @@ -3,29 +3,12 @@ package utils import ( "errors" "fmt" - "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "testing" + "github.com/owenrumney/go-sarif/v2/sarif" "github.com/stretchr/testify/assert" - "path/filepath" - "testing" ) -func TestRemoveDuplicateValues(t *testing.T) { - tests := []struct { - testedSlice []string - expectedResult []string - }{ - {testedSlice: []string{"1", "1", "1", "3"}, expectedResult: []string{"1", "3"}}, - {testedSlice: []string{}, expectedResult: []string{}}, - {testedSlice: []string{"1", "2", "3", "4"}, expectedResult: []string{"1", "2", "3", "4"}}, - {testedSlice: []string{"1", "6", "1", "6", "2"}, expectedResult: []string{"1", "6", "2"}}, - } - - for _, test := range tests { - assert.Equal(t, test.expectedResult, RemoveDuplicateValues(test.testedSlice)) - } -} - func TestGetResultFileName(t *testing.T) { fileNameValue := "fileNameValue" tests := []struct { @@ -42,12 +25,10 @@ func TestGetResultFileName(t *testing.T) { {PhysicalLocation: &sarif.PhysicalLocation{ArtifactLocation: &sarif.ArtifactLocation{URI: &fileNameValue}}}, }}, expectedOutput: fileNameValue}, - {result: &sarif.Result{}, - expectedOutput: ""}, } for _, test := range tests { - assert.Equal(t, test.expectedOutput, GetResultFileName(test.result)) + assert.Equal(t, test.expectedOutput, GetLocationFileName(test.result.Locations[0])) } } @@ -84,12 +65,10 @@ func TestGetResultLocationInFile(t *testing.T) { StartColumn: nil, }}}}}, expectedOutput: ""}, - {result: &sarif.Result{}, - expectedOutput: ""}, } for _, test := range tests { - assert.Equal(t, test.expectedOutput, GetResultLocationInFile(test.result)) + assert.Equal(t, test.expectedOutput, GetStartLocationInFile(test.result.Locations[0])) } } @@ -115,9 +94,11 @@ func TestExtractRelativePath(t *testing.T) { } func TestGetResultSeverity(t *testing.T) { - levelValueHigh := "error" - levelValueMedium := "warning" - levelValueLow := "info" + levelValueHigh := string(errorLevel) + levelValueMedium := string(warningLevel) + levelValueMedium2 := string(infoLevel) + levelValueLow := string(noteLevel) + levelValueUnknown := string(noneLevel) tests := []struct { result *sarif.Result @@ -129,8 +110,12 @@ func TestGetResultSeverity(t *testing.T) { expectedSeverity: "High"}, {result: &sarif.Result{Level: &levelValueMedium}, expectedSeverity: "Medium"}, + {result: &sarif.Result{Level: &levelValueMedium2}, + expectedSeverity: "Medium"}, {result: &sarif.Result{Level: &levelValueLow}, expectedSeverity: "Low"}, + {result: &sarif.Result{Level: &levelValueUnknown}, + expectedSeverity: "Unknown"}, } for _, test := range tests { @@ -140,7 +125,7 @@ func TestGetResultSeverity(t *testing.T) { func TestScanTypeErrorMsg(t *testing.T) { tests := []struct { - scanner ScanType + scanner JasScanType err error wantMsg string }{ @@ -187,36 +172,3 @@ func TestScanTypeErrorMsg(t *testing.T) { }) } } - -func TestGetFullPathsWorkingDirs(t *testing.T) { - currentDir, err := coreutils.GetWorkingDirectory() - assert.NoError(t, err) - dir1, err := filepath.Abs("dir1") - assert.NoError(t, err) - dir2, err := filepath.Abs("dir2") - assert.NoError(t, err) - tests := []struct { - name string - workingDirs []string - expectedDirs []string - }{ - { - name: "EmptyWorkingDirs", - workingDirs: []string{}, - expectedDirs: []string{currentDir}, - }, - { - name: "ValidWorkingDirs", - workingDirs: []string{"dir1", "dir2"}, - expectedDirs: []string{dir1, dir2}, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - actualDirs, err := GetFullPathsWorkingDirs(test.workingDirs) - assert.NoError(t, err) - assert.Equal(t, test.expectedDirs, actualDirs, "Incorrect full paths of working directories") - }) - } -} diff --git a/xray/utils/auditbasicparams.go b/xray/utils/auditbasicparams.go new file mode 100644 index 000000000..f8e3abba3 --- /dev/null +++ b/xray/utils/auditbasicparams.go @@ -0,0 +1,133 @@ +package utils + +import ( + "github.com/jfrog/jfrog-cli-core/v2/utils/config" + ioUtils "github.com/jfrog/jfrog-client-go/utils/io" +) + +type AuditBasicParams struct { + serverDetails *config.ServerDetails + outputFormat OutputFormat + progress ioUtils.ProgressMgr + directDependencies []string + excludeTestDependencies bool + useWrapper bool + insecureTls bool + pipRequirementsFile string + technologies []string + args []string + depsRepo string + ignoreConfigFile bool +} + +func (abp *AuditBasicParams) DirectDependencies() []string { + return abp.directDependencies +} + +func (abp *AuditBasicParams) AppendDirectDependencies(directDependencies []string) *AuditBasicParams { + abp.directDependencies = append(abp.directDependencies, directDependencies...) + return abp +} + +func (abp *AuditBasicParams) ServerDetails() (*config.ServerDetails, error) { + return abp.serverDetails, nil +} + +func (abp *AuditBasicParams) SetServerDetails(serverDetails *config.ServerDetails) *AuditBasicParams { + abp.serverDetails = serverDetails + return abp +} + +func (abp *AuditBasicParams) PipRequirementsFile() string { + return abp.pipRequirementsFile +} + +func (abp *AuditBasicParams) SetPipRequirementsFile(requirementsFile string) *AuditBasicParams { + abp.pipRequirementsFile = requirementsFile + return abp +} + +func (abp *AuditBasicParams) ExcludeTestDependencies() bool { + return abp.excludeTestDependencies +} + +func (abp *AuditBasicParams) SetExcludeTestDependencies(excludeTestDependencies bool) *AuditBasicParams { + abp.excludeTestDependencies = excludeTestDependencies + return abp +} + +func (abp *AuditBasicParams) UseWrapper() bool { + return abp.useWrapper +} + +func (abp *AuditBasicParams) SetUseWrapper(useWrapper bool) *AuditBasicParams { + abp.useWrapper = useWrapper + return abp +} + +func (abp *AuditBasicParams) InsecureTls() bool { + return abp.insecureTls +} + +func (abp *AuditBasicParams) SetInsecureTls(insecureTls bool) *AuditBasicParams { + abp.insecureTls = insecureTls + return abp +} + +func (abp *AuditBasicParams) Technologies() []string { + return abp.technologies +} + +func (abp *AuditBasicParams) SetTechnologies(technologies []string) *AuditBasicParams { + abp.technologies = technologies + return abp +} + +func (abp *AuditBasicParams) Progress() ioUtils.ProgressMgr { + return abp.progress +} + +func (abp *AuditBasicParams) SetProgress(progress ioUtils.ProgressMgr) { + abp.progress = progress +} + +func (abp *AuditBasicParams) Args() []string { + return abp.args +} + +func (abp *AuditBasicParams) SetNpmScope(depType string) *AuditBasicParams { + switch depType { + case "devOnly": + abp.args = []string{"--dev"} + case "prodOnly": + abp.args = []string{"--prod"} + } + return abp +} + +func (abp *AuditBasicParams) OutputFormat() OutputFormat { + return abp.outputFormat +} + +func (abp *AuditBasicParams) SetOutputFormat(format OutputFormat) *AuditBasicParams { + abp.outputFormat = format + return abp +} + +func (abp *AuditBasicParams) DepsRepo() string { + return abp.depsRepo +} + +func (abp *AuditBasicParams) SetDepsRepo(depsRepo string) *AuditBasicParams { + abp.depsRepo = depsRepo + return abp +} + +func (abp *AuditBasicParams) IgnoreConfigFile() bool { + return abp.ignoreConfigFile +} + +func (abp *AuditBasicParams) SetIgnoreConfigFile(ignoreConfigFile bool) *AuditBasicParams { + abp.ignoreConfigFile = ignoreConfigFile + return abp +} diff --git a/xray/utils/models.go b/xray/utils/models.go deleted file mode 100644 index 553758082..000000000 --- a/xray/utils/models.go +++ /dev/null @@ -1,133 +0,0 @@ -package utils - -import ( - "github.com/jfrog/jfrog-cli-core/v2/utils/config" - ioUtils "github.com/jfrog/jfrog-client-go/utils/io" -) - -type GraphBasicParams struct { - serverDetails *config.ServerDetails - outputFormat OutputFormat - progress ioUtils.ProgressMgr - directDependencies []string - excludeTestDependencies bool - useWrapper bool - insecureTls bool - pipRequirementsFile string - technologies []string - args []string - depsRepo string - ignoreConfigFile bool -} - -func (gbp *GraphBasicParams) DirectDependencies() []string { - return gbp.directDependencies -} - -func (gbp *GraphBasicParams) AppendDirectDependencies(directDependencies []string) *GraphBasicParams { - gbp.directDependencies = append(gbp.directDependencies, directDependencies...) - return gbp -} - -func (gbp *GraphBasicParams) ServerDetails() (*config.ServerDetails, error) { - return gbp.serverDetails, nil -} - -func (gbp *GraphBasicParams) SetServerDetails(serverDetails *config.ServerDetails) *GraphBasicParams { - gbp.serverDetails = serverDetails - return gbp -} - -func (gbp *GraphBasicParams) PipRequirementsFile() string { - return gbp.pipRequirementsFile -} - -func (gbp *GraphBasicParams) SetPipRequirementsFile(requirementsFile string) *GraphBasicParams { - gbp.pipRequirementsFile = requirementsFile - return gbp -} - -func (gbp *GraphBasicParams) ExcludeTestDependencies() bool { - return gbp.excludeTestDependencies -} - -func (gbp *GraphBasicParams) SetExcludeTestDependencies(excludeTestDependencies bool) *GraphBasicParams { - gbp.excludeTestDependencies = excludeTestDependencies - return gbp -} - -func (gbp *GraphBasicParams) UseWrapper() bool { - return gbp.useWrapper -} - -func (gbp *GraphBasicParams) SetUseWrapper(useWrapper bool) *GraphBasicParams { - gbp.useWrapper = useWrapper - return gbp -} - -func (gbp *GraphBasicParams) InsecureTls() bool { - return gbp.insecureTls -} - -func (gbp *GraphBasicParams) SetInsecureTls(insecureTls bool) *GraphBasicParams { - gbp.insecureTls = insecureTls - return gbp -} - -func (gbp *GraphBasicParams) Technologies() []string { - return gbp.technologies -} - -func (gbp *GraphBasicParams) SetTechnologies(technologies []string) *GraphBasicParams { - gbp.technologies = technologies - return gbp -} - -func (gbp *GraphBasicParams) Progress() ioUtils.ProgressMgr { - return gbp.progress -} - -func (gbp *GraphBasicParams) SetProgress(progress ioUtils.ProgressMgr) { - gbp.progress = progress -} - -func (gbp *GraphBasicParams) Args() []string { - return gbp.args -} - -func (gbp *GraphBasicParams) SetNpmScope(depType string) *GraphBasicParams { - switch depType { - case "devOnly": - gbp.args = []string{"--dev"} - case "prodOnly": - gbp.args = []string{"--prod"} - } - return gbp -} - -func (gbp *GraphBasicParams) OutputFormat() OutputFormat { - return gbp.outputFormat -} - -func (gbp *GraphBasicParams) SetOutputFormat(format OutputFormat) *GraphBasicParams { - gbp.outputFormat = format - return gbp -} - -func (gbp *GraphBasicParams) DepsRepo() string { - return gbp.depsRepo -} - -func (gbp *GraphBasicParams) SetDepsRepo(depsRepo string) *GraphBasicParams { - gbp.depsRepo = depsRepo - return gbp -} - -func (gbp *GraphBasicParams) IgnoreConfigFile() bool { - return gbp.ignoreConfigFile -} - -func (gbp *GraphBasicParams) SetIgnoreConfigFile(ignoreConfigFile bool) *GraphBasicParams { - gbp.ignoreConfigFile = ignoreConfigFile - return gbp -} diff --git a/xray/utils/resultstable.go b/xray/utils/resultstable.go index 745b61b46..42f20a864 100644 --- a/xray/utils/resultstable.go +++ b/xray/utils/resultstable.go @@ -2,15 +2,17 @@ package utils import ( "fmt" - "github.com/jfrog/gofrog/datastructures" - "golang.org/x/exp/maps" - "golang.org/x/text/cases" - "golang.org/x/text/language" "os" "sort" "strconv" "strings" + "github.com/jfrog/gofrog/datastructures" + "github.com/owenrumney/go-sarif/v2/sarif" + "golang.org/x/exp/maps" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "github.com/jfrog/jfrog-cli-core/v2/xray/formats" "github.com/gookit/color" @@ -87,6 +89,9 @@ func prepareViolations(violations []services.Violation, extendedResults *Extende case "security": cves := convertCves(violation.Cves) applicableValue := getApplicableCveValue(extendedResults, cves) + for _, cve := range cves { + cve.Applicability = getCveApplicability(cve, extendedResults.ApplicabilityScanResults) + } currSeverity := GetSeverity(violation.Severity, applicableValue) jfrogResearchInfo := convertJfrogResearchInformation(violation.ExtendedInformation) for compIndex := 0; compIndex < len(impactedPackagesNames); compIndex++ { @@ -111,7 +116,7 @@ func prepareViolations(violations []services.Violation, extendedResults *Extende ) } case "license": - currSeverity := GetSeverity(violation.Severity, ApplicabilityUndeterminedStringValue) + currSeverity := GetSeverity(violation.Severity, ApplicabilityUndetermined) for compIndex := 0; compIndex < len(impactedPackagesNames); compIndex++ { licenseViolationsRows = append(licenseViolationsRows, formats.LicenseViolationRow{ @@ -126,7 +131,7 @@ func prepareViolations(violations []services.Violation, extendedResults *Extende ) } case "operational_risk": - currSeverity := GetSeverity(violation.Severity, ApplicabilityUndeterminedStringValue) + currSeverity := GetSeverity(violation.Severity, ApplicabilityUndetermined) violationOpRiskData := getOperationalRiskViolationReadableData(violation) for compIndex := 0; compIndex < len(impactedPackagesNames); compIndex++ { operationalRiskViolationsRow := &formats.OperationalRiskViolationRow{ @@ -204,6 +209,9 @@ func prepareVulnerabilities(vulnerabilities []services.Vulnerability, extendedRe } cves := convertCves(vulnerability.Cves) applicableValue := getApplicableCveValue(extendedResults, cves) + for _, cve := range cves { + cve.Applicability = getCveApplicability(cve, extendedResults.ApplicabilityScanResults) + } currSeverity := GetSeverity(vulnerability.Severity, applicableValue) jfrogResearchInfo := convertJfrogResearchInformation(vulnerability.ExtendedInformation) for compIndex := 0; compIndex < len(impactedPackagesNames); compIndex++ { @@ -283,24 +291,30 @@ func PrepareLicenses(licenses []services.License) ([]formats.LicenseRow, error) } // Prepare secrets for all non-table formats (without style or emoji) -func PrepareSecrets(secrets []IacOrSecretResult) []formats.IacSecretsRow { +func PrepareSecrets(secrets []*sarif.Run) []formats.SourceCodeRow { return prepareSecrets(secrets, false) } -func prepareSecrets(secrets []IacOrSecretResult, isTable bool) []formats.IacSecretsRow { - var secretsRows []formats.IacSecretsRow - for _, secret := range secrets { - currSeverity := GetSeverity(secret.Severity, ApplicableStringValue) - secretsRows = append(secretsRows, - formats.IacSecretsRow{ - Severity: currSeverity.printableTitle(isTable), - SeverityNumValue: currSeverity.numValue, - File: secret.File, - LineColumn: secret.LineColumn, - Text: secret.Text, - Type: secret.Type, - }, - ) +func prepareSecrets(secrets []*sarif.Run, isTable bool) []formats.SourceCodeRow { + var secretsRows []formats.SourceCodeRow + for _, secretRun := range secrets { + for _, secret := range secretRun.Results { + currSeverity := GetSeverity(GetResultSeverity(secret), Applicable) + for _, location := range secret.Locations { + secretsRows = append(secretsRows, + formats.SourceCodeRow{ + Severity: currSeverity.printableTitle(isTable), + SeverityNumValue: currSeverity.numValue, + SourceCodeLocationRow: formats.SourceCodeLocationRow{ + File: GetLocationFileName(location), + LineColumn: GetStartLocationInFile(location), + Snippet: GetLocationSnippet(location), + }, + Type: *secret.RuleID, + }, + ) + } + } } sort.Slice(secretsRows, func(i, j int) bool { @@ -310,7 +324,7 @@ func prepareSecrets(secrets []IacOrSecretResult, isTable bool) []formats.IacSecr return secretsRows } -func PrintSecretsTable(secrets []IacOrSecretResult, entitledForSecretsScan bool) error { +func PrintSecretsTable(secrets []*sarif.Run, entitledForSecretsScan bool) error { if entitledForSecretsScan { secretsRows := prepareSecrets(secrets, true) log.Output() @@ -321,24 +335,30 @@ func PrintSecretsTable(secrets []IacOrSecretResult, entitledForSecretsScan bool) } // Prepare iacs for all non-table formats (without style or emoji) -func PrepareIacs(iacs []IacOrSecretResult) []formats.IacSecretsRow { +func PrepareIacs(iacs []*sarif.Run) []formats.SourceCodeRow { return prepareIacs(iacs, false) } -func prepareIacs(iacs []IacOrSecretResult, isTable bool) []formats.IacSecretsRow { - var iacRows []formats.IacSecretsRow - for _, iac := range iacs { - currSeverity := GetSeverity(iac.Severity, ApplicableStringValue) - iacRows = append(iacRows, - formats.IacSecretsRow{ - Severity: currSeverity.printableTitle(isTable), - SeverityNumValue: currSeverity.numValue, - File: iac.File, - LineColumn: iac.LineColumn, - Text: iac.Text, - Type: iac.Type, - }, - ) +func prepareIacs(iacs []*sarif.Run, isTable bool) []formats.SourceCodeRow { + var iacRows []formats.SourceCodeRow + for _, iacRun := range iacs { + for _, iac := range iacRun.Results { + currSeverity := GetSeverity(GetResultSeverity(iac), Applicable) + for _, location := range iac.Locations { + iacRows = append(iacRows, + formats.SourceCodeRow{ + Severity: currSeverity.printableTitle(isTable), + SeverityNumValue: currSeverity.numValue, + SourceCodeLocationRow: formats.SourceCodeLocationRow{ + File: GetLocationFileName(location), + LineColumn: GetStartLocationInFile(location), + Snippet: GetResultMsgText(iac), + }, + Type: *iac.RuleID, + }, + ) + } + } } sort.Slice(iacRows, func(i, j int) bool { @@ -348,7 +368,7 @@ func prepareIacs(iacs []IacOrSecretResult, isTable bool) []formats.IacSecretsRow return iacRows } -func PrintIacTable(iacs []IacOrSecretResult, entitledForIacScan bool) error { +func PrintIacTable(iacs []*sarif.Run, entitledForIacScan bool) error { if entitledForIacScan { iacRows := prepareIacs(iacs, true) log.Output() @@ -358,12 +378,71 @@ func PrintIacTable(iacs []IacOrSecretResult, entitledForIacScan bool) error { return nil } -func convertCves(cves []services.Cve) []formats.CveRow { - var cveRows []formats.CveRow - for _, cveObj := range cves { - cveRows = append(cveRows, formats.CveRow{Id: cveObj.Id, CvssV2: cveObj.CvssV2Score, CvssV3: cveObj.CvssV3Score}) +func PrepareSast(sasts []*sarif.Run) []formats.SourceCodeRow { + return prepareSast(sasts, false) +} + +func prepareSast(sasts []*sarif.Run, isTable bool) []formats.SourceCodeRow { + var sastRows []formats.SourceCodeRow + for _, sastRun := range sasts { + for _, sast := range sastRun.Results { + currSeverity := GetSeverity(GetResultSeverity(sast), Applicable) + + flows := toSourceCodeCodeFlowRow(sast.CodeFlows, isTable) + for _, location := range sast.Locations { + sastRows = append(sastRows, + formats.SourceCodeRow{ + Severity: currSeverity.printableTitle(isTable), + SeverityNumValue: currSeverity.numValue, + SourceCodeLocationRow: formats.SourceCodeLocationRow{ + File: GetLocationFileName(location), + LineColumn: GetStartLocationInFile(location), + Snippet: GetResultMsgText(sast), + }, + Type: *sast.RuleID, + CodeFlow: flows, + }, + ) + } + } } - return cveRows + + sort.Slice(sastRows, func(i, j int) bool { + return sastRows[i].SeverityNumValue > sastRows[j].SeverityNumValue + }) + + return sastRows +} + +func toSourceCodeCodeFlowRow(flows []*sarif.CodeFlow, isTable bool) (flowRows [][]formats.SourceCodeLocationRow) { + if isTable { + // Not displaying in table + return + } + for _, codeFlow := range flows { + for _, stackTrace := range codeFlow.ThreadFlows { + rowFlow := []formats.SourceCodeLocationRow{} + for _, stackTraceEntry := range stackTrace.Locations { + rowFlow = append(rowFlow, formats.SourceCodeLocationRow{ + File: GetLocationFileName(stackTraceEntry.Location), + LineColumn: GetStartLocationInFile(stackTraceEntry.Location), + Snippet: GetLocationSnippet(stackTraceEntry.Location), + }) + } + flowRows = append(flowRows, rowFlow) + } + } + return +} + +func PrintSastTable(sast []*sarif.Run, entitledForSastScan bool) error { + if entitledForSastScan { + sastRows := prepareSast(sast, true) + log.Output() + return coreutils.PrintTable(formats.ConvertToSastTableRow(sastRows), "Static Application Security Testing (SAST)", + "✨ No Static Application Security Testing vulnerabilities were found ✨", false) + } + return nil } func convertJfrogResearchInformation(extendedInfo *services.ExtendedInformation) *formats.JfrogResearchInformation { @@ -534,26 +613,31 @@ func (s *Severity) printableTitle(isTable bool) string { return s.title } -var Severities = map[string]map[string]*Severity{ +var Severities = map[string]map[ApplicabilityStatus]*Severity{ "Critical": { - ApplicableStringValue: {emoji: "💀", title: "Critical", numValue: 12, style: color.New(color.BgLightRed, color.LightWhite)}, - ApplicabilityUndeterminedStringValue: {emoji: "💀", title: "Critical", numValue: 11, style: color.New(color.BgLightRed, color.LightWhite)}, - NotApplicableStringValue: {emoji: "💀", title: "Critical", numValue: 4, style: color.New(color.Gray)}, + Applicable: {emoji: "💀", title: "Critical", numValue: 15, style: color.New(color.BgLightRed, color.LightWhite)}, + ApplicabilityUndetermined: {emoji: "💀", title: "Critical", numValue: 14, style: color.New(color.BgLightRed, color.LightWhite)}, + NotApplicable: {emoji: "💀", title: "Critical", numValue: 5, style: color.New(color.Gray)}, }, "High": { - ApplicableStringValue: {emoji: "🔥", title: "High", numValue: 10, style: color.New(color.Red)}, - ApplicabilityUndeterminedStringValue: {emoji: "🔥", title: "High", numValue: 9, style: color.New(color.Red)}, - NotApplicableStringValue: {emoji: "🔥", title: "High", numValue: 3, style: color.New(color.Gray)}, + Applicable: {emoji: "🔥", title: "High", numValue: 13, style: color.New(color.Red)}, + ApplicabilityUndetermined: {emoji: "🔥", title: "High", numValue: 12, style: color.New(color.Red)}, + NotApplicable: {emoji: "🔥", title: "High", numValue: 4, style: color.New(color.Gray)}, }, "Medium": { - ApplicableStringValue: {emoji: "🎃", title: "Medium", numValue: 8, style: color.New(color.Yellow)}, - ApplicabilityUndeterminedStringValue: {emoji: "🎃", title: "Medium", numValue: 7, style: color.New(color.Yellow)}, - NotApplicableStringValue: {emoji: "🎃", title: "Medium", numValue: 2, style: color.New(color.Gray)}, + Applicable: {emoji: "🎃", title: "Medium", numValue: 11, style: color.New(color.Yellow)}, + ApplicabilityUndetermined: {emoji: "🎃", title: "Medium", numValue: 10, style: color.New(color.Yellow)}, + NotApplicable: {emoji: "🎃", title: "Medium", numValue: 3, style: color.New(color.Gray)}, }, "Low": { - ApplicableStringValue: {emoji: "👻", title: "Low", numValue: 6}, - ApplicabilityUndeterminedStringValue: {emoji: "👻", title: "Low", numValue: 5}, - NotApplicableStringValue: {emoji: "👻", title: "Low", numValue: 1, style: color.New(color.Gray)}, + Applicable: {emoji: "👻", title: "Low", numValue: 9}, + ApplicabilityUndetermined: {emoji: "👻", title: "Low", numValue: 8}, + NotApplicable: {emoji: "👻", title: "Low", numValue: 2, style: color.New(color.Gray)}, + }, + "Unknown": { + Applicable: {emoji: "😐", title: "Unknown", numValue: 7}, + ApplicabilityUndetermined: {emoji: "😐", title: "Unknown", numValue: 6}, + NotApplicable: {emoji: "😐", title: "Unknown", numValue: 1, style: color.New(color.Gray)}, }, } @@ -567,25 +651,25 @@ func (s *Severity) Emoji() string { func GetSeveritiesFormat(severity string) (string, error) { formattedSeverity := cases.Title(language.Und).String(severity) - if formattedSeverity != "" && Severities[formattedSeverity][ApplicableStringValue] == nil { + if formattedSeverity != "" && Severities[formattedSeverity][Applicable] == nil { return "", errorutils.CheckErrorf("only the following severities are supported: " + coreutils.ListToText(maps.Keys(Severities))) } return formattedSeverity, nil } -func GetSeverity(severityTitle string, applicable string) *Severity { +func GetSeverity(severityTitle string, applicable ApplicabilityStatus) *Severity { if Severities[severityTitle] == nil { return &Severity{title: severityTitle} } switch applicable { - case NotApplicableStringValue: - return Severities[severityTitle][NotApplicableStringValue] - case ApplicableStringValue: - return Severities[severityTitle][ApplicableStringValue] + case NotApplicable: + return Severities[severityTitle][NotApplicable] + case Applicable: + return Severities[severityTitle][Applicable] default: - return Severities[severityTitle][ApplicabilityUndeterminedStringValue] + return Severities[severityTitle][ApplicabilityUndetermined] } } @@ -804,41 +888,86 @@ func GetUniqueKey(vulnerableDependency, vulnerableVersion, xrayID string, fixVer return strings.Join([]string{vulnerableDependency, vulnerableVersion, xrayID, strconv.FormatBool(fixVersionExist)}, ":") } +func convertCves(cves []services.Cve) []formats.CveRow { + var cveRows []formats.CveRow + for _, cveObj := range cves { + cveRows = append(cveRows, formats.CveRow{Id: cveObj.Id, CvssV2: cveObj.CvssV2Score, CvssV3: cveObj.CvssV3Score}) + } + return cveRows +} + // If at least one cve is applicable - final value is applicable // Else if at least one cve is undetermined - final value is undetermined // Else (case when all cves aren't applicable) -> final value is not applicable -func getApplicableCveValue(extendedResults *ExtendedScanResults, xrayCves []formats.CveRow) string { +func getApplicableCveValue(extendedResults *ExtendedScanResults, xrayCves []formats.CveRow) ApplicabilityStatus { if !extendedResults.EntitledForJas || len(extendedResults.ApplicabilityScanResults) == 0 { - return "" + return NotScanned } if len(xrayCves) == 0 { - return ApplicabilityUndeterminedStringValue + return ApplicabilityUndetermined } cveExistsInResult := false - finalApplicableValue := NotApplicableStringValue - for _, cve := range xrayCves { - if currentCveApplicableValue, exists := extendedResults.ApplicabilityScanResults[cve.Id]; exists { - cveExistsInResult = true - if currentCveApplicableValue == ApplicableStringValue { - return currentCveApplicableValue - } else if currentCveApplicableValue == ApplicabilityUndeterminedStringValue { - finalApplicableValue = currentCveApplicableValue + finalApplicableValue := NotApplicable + for _, applicabilityRun := range extendedResults.ApplicabilityScanResults { + for _, cve := range xrayCves { + relatedResults := GetResultsByRuleId(applicabilityRun, GetRuleIdFromCveId(cve.Id)) + if len(relatedResults) == 0 { + finalApplicableValue = ApplicabilityUndetermined + } + for _, relatedResult := range relatedResults { + cveExistsInResult = true + if isApplicableResult(relatedResult) { + return Applicable + } } } } if cveExistsInResult { return finalApplicableValue } - return ApplicabilityUndeterminedStringValue + return ApplicabilityUndetermined +} + +func getCveApplicability(cve formats.CveRow, applicabilityScanResults []*sarif.Run) (applicability *formats.Applicability) { + if len(applicabilityScanResults) == 0 { + return nil + } + for _, applicabilityRun := range applicabilityScanResults { + description := "" + if relatedRule, _ := applicabilityRun.GetRuleById(GetRuleIdFromCveId(cve.Id)); relatedRule != nil { + description = GetRuleFullDescription(relatedRule) + } + relatedResult, _ := applicabilityRun.GetResultByRuleId(GetRuleIdFromCveId(cve.Id)) + if relatedResult == nil { + continue + } + // Set applicable details + applicability = &formats.Applicability{ + Status: isApplicableResult(relatedResult), + ScannerDescription: description, + } + // Add new evidences from locations + for _, location := range relatedResult.Locations { + applicability.Evidence = append(applicability.Evidence, formats.Evidence{ + SourceCodeLocationRow: formats.SourceCodeLocationRow{ + File: GetLocationFileName(location), + LineColumn: GetStartLocationInFile(location), + Snippet: GetLocationSnippet(location), + }, + Reason: GetResultMsgText(relatedResult), + }) + } + } + return } -func printApplicableCveValue(applicableValue string, isTable bool) string { +func printApplicableCveValue(applicableValue ApplicabilityStatus, isTable bool) string { if isTable && (log.IsStdOutTerminal() && log.IsColorsSupported() || os.Getenv("GITLAB_CI") != "") { - if applicableValue == ApplicableStringValue { + if applicableValue == Applicable { return color.New(color.Red).Render(applicableValue) - } else if applicableValue == NotApplicableStringValue { + } else if applicableValue == NotApplicable { return color.New(color.Green).Render(applicableValue) } } - return applicableValue + return string(applicableValue) } diff --git a/xray/utils/resultstable_test.go b/xray/utils/resultstable_test.go index 0f102862d..8dad92cef 100644 --- a/xray/utils/resultstable_test.go +++ b/xray/utils/resultstable_test.go @@ -3,9 +3,11 @@ package utils import ( "errors" "fmt" - "github.com/jfrog/jfrog-cli-core/v2/xray/formats" "testing" + "github.com/jfrog/jfrog-cli-core/v2/xray/formats" + "github.com/owenrumney/go-sarif/v2/sarif" + "github.com/jfrog/jfrog-client-go/xray/services" "github.com/stretchr/testify/assert" ) @@ -426,38 +428,110 @@ func TestGetSeveritiesFormat(t *testing.T) { func TestGetApplicableCveValue(t *testing.T) { testCases := []struct { scanResults *ExtendedScanResults - cves []formats.CveRow - expectedResult string + cves []services.Cve + expectedResult ApplicabilityStatus + expectedCves []formats.CveRow }{ - {scanResults: &ExtendedScanResults{EntitledForJas: false}, expectedResult: ""}, - {scanResults: &ExtendedScanResults{ - ApplicabilityScanResults: map[string]string{"testCve1": ApplicableStringValue, "testCve2": NotApplicableStringValue}, - EntitledForJas: true}, - cves: nil, expectedResult: ApplicabilityUndeterminedStringValue}, - {scanResults: &ExtendedScanResults{ - ApplicabilityScanResults: map[string]string{"testCve1": NotApplicableStringValue, "testCve2": ApplicableStringValue}, - EntitledForJas: true}, - cves: []formats.CveRow{{Id: "testCve2"}}, expectedResult: ApplicableStringValue}, - {scanResults: &ExtendedScanResults{ - ApplicabilityScanResults: map[string]string{"testCve1": NotApplicableStringValue, "testCve2": ApplicableStringValue}, - EntitledForJas: true}, - cves: []formats.CveRow{{Id: "testCve3"}}, expectedResult: ApplicabilityUndeterminedStringValue}, - {scanResults: &ExtendedScanResults{ - ApplicabilityScanResults: map[string]string{"testCve1": NotApplicableStringValue, "testCve2": NotApplicableStringValue}, - EntitledForJas: true}, - cves: []formats.CveRow{{Id: "testCve1"}, {Id: "testCve2"}}, expectedResult: NotApplicableStringValue}, - {scanResults: &ExtendedScanResults{ - ApplicabilityScanResults: map[string]string{"testCve1": NotApplicableStringValue, "testCve2": ApplicableStringValue}, - EntitledForJas: true}, - cves: []formats.CveRow{{Id: "testCve1"}, {Id: "testCve2"}}, expectedResult: ApplicableStringValue}, - {scanResults: &ExtendedScanResults{ - ApplicabilityScanResults: map[string]string{"testCve1": NotApplicableStringValue, "testCve2": ApplicabilityUndeterminedStringValue}, - EntitledForJas: true}, - cves: []formats.CveRow{{Id: "testCve1"}, {Id: "testCve2"}}, expectedResult: ApplicabilityUndeterminedStringValue}, + { + scanResults: &ExtendedScanResults{EntitledForJas: false}, + expectedResult: NotScanned, + }, + { + scanResults: &ExtendedScanResults{ + ApplicabilityScanResults: []*sarif.Run{ + getRunWithDummyResults( + getDummyResultWithOneLocation("fileName1", 0, 1, "snippet1", "applic_testCve1", "info"), + getDummyPassingResult("applic_testCve2"), + ), + }, + EntitledForJas: true, + }, + cves: nil, + expectedResult: ApplicabilityUndetermined, + expectedCves: nil, + }, + { + scanResults: &ExtendedScanResults{ + ApplicabilityScanResults: []*sarif.Run{ + getRunWithDummyResults( + getDummyPassingResult("applic_testCve1"), + getDummyResultWithOneLocation("fileName2", 1, 0, "snippet2", "applic_testCve2", "warning"), + ), + }, + EntitledForJas: true, + }, + cves: []services.Cve{{Id: "testCve2"}}, + expectedResult: Applicable, + expectedCves: []formats.CveRow{{Id: "testCve2", Applicability: &formats.Applicability{Status: true}}}, + }, + { + scanResults: &ExtendedScanResults{ + ApplicabilityScanResults: []*sarif.Run{ + getRunWithDummyResults( + getDummyPassingResult("applic_testCve1"), + getDummyResultWithOneLocation("fileName3", 0, 1, "snippet3", "applic_testCve2", "info"), + ), + }, + EntitledForJas: true, + }, + cves: []services.Cve{{Id: "testCve3"}}, + expectedResult: ApplicabilityUndetermined, + expectedCves: []formats.CveRow{{Id: "testCve3"}}, + }, + { + scanResults: &ExtendedScanResults{ + ApplicabilityScanResults: []*sarif.Run{ + getRunWithDummyResults( + getDummyPassingResult("applic_testCve1"), + getDummyPassingResult("applic_testCve2"), + ), + }, + EntitledForJas: true, + }, + cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}}, + expectedResult: NotApplicable, + expectedCves: []formats.CveRow{{Id: "testCve1", Applicability: &formats.Applicability{Status: false}}, {Id: "testCve2", Applicability: &formats.Applicability{Status: false}}}, + }, + { + scanResults: &ExtendedScanResults{ + ApplicabilityScanResults: []*sarif.Run{ + getRunWithDummyResults( + getDummyPassingResult("applic_testCve1"), + getDummyResultWithOneLocation("fileName4", 1, 0, "snippet", "applic_testCve2", "warning"), + ), + }, + EntitledForJas: true, + }, + cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}}, + expectedResult: Applicable, + expectedCves: []formats.CveRow{{Id: "testCve1", Applicability: &formats.Applicability{Status: false}}, {Id: "testCve2", Applicability: &formats.Applicability{Status: true}}}, + }, + { + scanResults: &ExtendedScanResults{ + ApplicabilityScanResults: []*sarif.Run{ + getRunWithDummyResults(getDummyPassingResult("applic_testCve1")), + }, + EntitledForJas: true}, + cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}}, + expectedResult: ApplicabilityUndetermined, + expectedCves: []formats.CveRow{{Id: "testCve1", Applicability: &formats.Applicability{Status: false}}, {Id: "testCve2"}}, + }, } for _, testCase := range testCases { - assert.Equal(t, testCase.expectedResult, getApplicableCveValue(testCase.scanResults, testCase.cves)) + cves := convertCves(testCase.cves) + applicableValue := getApplicableCveValue(testCase.scanResults, cves) + for i := range cves { + cves[i].Applicability = getCveApplicability(cves[i], testCase.scanResults.ApplicabilityScanResults) + } + assert.Equal(t, testCase.expectedResult, applicableValue) + if assert.True(t, len(testCase.expectedCves) == len(cves)) { + for i := range cves { + if testCase.expectedCves[i].Applicability != nil && assert.NotNil(t, cves[i].Applicability) { + assert.Equal(t, testCase.expectedCves[i].Applicability.Status, cves[i].Applicability.Status) + } + } + } } } @@ -525,7 +599,7 @@ func TestSortVulnerabilityOrViolationRows(t *testing.T) { { Summary: "Summary 1", Severity: "Critical", - Applicable: ApplicableStringValue, + Applicable: string(Applicable), SeverityNumValue: 13, FixedVersions: []string{"1.0.0"}, ImpactedDependencyName: "Dependency 1", @@ -533,7 +607,7 @@ func TestSortVulnerabilityOrViolationRows(t *testing.T) { }, { Summary: "Summary 2", - Applicable: NotApplicableStringValue, + Applicable: string(NotApplicable), Severity: "Critical", SeverityNumValue: 11, ImpactedDependencyName: "Dependency 2", @@ -541,7 +615,7 @@ func TestSortVulnerabilityOrViolationRows(t *testing.T) { }, { Summary: "Summary 3", - Applicable: ApplicabilityUndeterminedStringValue, + Applicable: string(ApplicabilityUndetermined), Severity: "Critical", SeverityNumValue: 12, ImpactedDependencyName: "Dependency 3", diff --git a/xray/utils/resultwriter.go b/xray/utils/resultwriter.go index 33f5ed3b2..e2a82b2a8 100644 --- a/xray/utils/resultwriter.go +++ b/xray/utils/resultwriter.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/json" "fmt" - "os" "strconv" "strings" @@ -28,26 +27,13 @@ const ( Sarif OutputFormat = "sarif" ) -const missingCveScore = "0" +const MissingCveScore = "0" const maxPossibleCve = 10.0 var OutputFormats = []string{string(Table), string(Json), string(SimpleJson), string(Sarif)} var CurationOutputFormats = []string{string(Table), string(Json)} -type sarifProperties struct { - Applicable string - Cves string - Headline string - Severity string - Description string - MarkdownDescription string - XrayID string - File string - LineColumn string - SecretsOrIacType string -} - // PrintScanResults prints the scan results in the specified format. // Note that errors are printed only with SimpleJson format. // @@ -73,7 +59,7 @@ func PrintScanResults(results *ExtendedScanResults, simpleJsonError []formats.Si case Json: return PrintJson(results.getXrayScanResults()) case Sarif: - sarifFile, err := GenerateSarifFileFromScan(results, isMultipleRoots, false, "JFrog Security", coreutils.JFrogComUrl+"xray/") + sarifFile, err := GenerateSarifContentFromResults(results, isMultipleRoots, includeLicenses, false) if err != nil { return err } @@ -106,10 +92,48 @@ func printScanResultsTables(results *ExtendedScanResults, isBinaryScan, includeV return } } + ConvertRunsPathsToRelative(results.SecretsScanResults) if err = PrintSecretsTable(results.SecretsScanResults, results.EntitledForJas); err != nil { return } - return PrintIacTable(results.IacScanResults, results.EntitledForJas) + ConvertRunsPathsToRelative(results.IacScanResults) + if err = PrintIacTable(results.IacScanResults, results.EntitledForJas); err != nil { + return + } + if !IsSastSupported() { + return + } + ConvertRunsPathsToRelative(results.SastScanResults) + return PrintSastTable(results.SastScanResults, results.EntitledForJas) +} + +// The paths at Sarif runs are absolute. +// Use this method if you need to translate the file paths to relative +func ConvertRunsPathsToRelative(runs []*sarif.Run) { + for _, sarifRun := range runs { + for _, invocation := range sarifRun.Invocations { + if wd := GetInvocationWorkingDirectory(invocation); len(wd) > 0 { + ConvertRunPathsToRelative(sarifRun, wd) + } + } + } +} + +func ConvertRunPathsToRelative(sarifRun *sarif.Run, wd string) { + for _, sarifResult := range sarifRun.Results { + // Convert paths in locations + for _, location := range sarifResult.Locations { + SetLocationFileName(location, ExtractRelativePath(GetLocationFileName(location), wd)) + } + // Convert paths in code flows + for _, codeFlows := range sarifResult.CodeFlows { + for _, threadFlows := range codeFlows.ThreadFlows { + for _, location := range threadFlows.Locations { + SetLocationFileName(location.Location, ExtractRelativePath(GetLocationFileName(location.Location), wd)) + } + } + } + } } func printMessages(messages []string) { @@ -125,16 +149,22 @@ func printMessage(message string) { log.Output("💬" + message) } -func GenerateSarifFileFromScan(extendedResults *ExtendedScanResults, isMultipleRoots, markdownOutput bool, scanningTool, toolURI string) (string, error) { - report, err := sarif.New(sarif.Version210) +func GenerateSarifContentFromResults(extendedResults *ExtendedScanResults, isMultipleRoots, includeLicenses, markdownOutput bool) (sarifStr string, err error) { + report, err := NewReport() if err != nil { - return "", errorutils.CheckError(err) + return } - run := sarif.NewRunWithInformationURI(scanningTool, toolURI) - if err = convertScanToSarif(run, extendedResults, isMultipleRoots, markdownOutput); err != nil { - return "", err + xrayRun, err := convertXrayResponsesToSarifRun(extendedResults, isMultipleRoots, includeLicenses, markdownOutput) + if err != nil { + return } - report.AddRun(run) + + report.Runs = append(report.Runs, xrayRun) + report.Runs = append(report.Runs, extendedResults.ApplicabilityScanResults...) + report.Runs = append(report.Runs, extendedResults.IacScanResults...) + report.Runs = append(report.Runs, extendedResults.SecretsScanResults...) + report.Runs = append(report.Runs, extendedResults.SastScanResults...) + out, err := json.Marshal(report) if err != nil { return "", errorutils.CheckError(err) @@ -143,7 +173,109 @@ func GenerateSarifFileFromScan(extendedResults *ExtendedScanResults, isMultipleR return clientUtils.IndentJson(out), nil } -func convertScanToSimpleJson(extendedResults *ExtendedScanResults, errors []formats.SimpleJsonError, isMultipleRoots, includeLicenses, simplifiedOutput bool) (formats.SimpleJsonResults, error) { +func convertXrayResponsesToSarifRun(extendedResults *ExtendedScanResults, isMultipleRoots, includeLicenses, markdownOutput bool) (run *sarif.Run, err error) { + xrayJson, err := convertXrayScanToSimpleJson(extendedResults, isMultipleRoots, includeLicenses, true) + if err != nil { + return + } + xrayRun := sarif.NewRunWithInformationURI("JFrog Xray Sca", "https://jfrog.com/xray/") + xrayRun.Tool.Driver.Version = &extendedResults.XrayVersion + if len(xrayJson.Vulnerabilities) > 0 || len(xrayJson.SecurityViolations) > 0 { + if err = extractXrayIssuesToSarifRun(xrayRun, xrayJson, markdownOutput); err != nil { + return + } + } + run = xrayRun + return +} + +func extractXrayIssuesToSarifRun(run *sarif.Run, xrayJson formats.SimpleJsonResults, markdownOutput bool) error { + for _, vulnerability := range xrayJson.Vulnerabilities { + if err := addXrayCveIssueToSarifRun( + vulnerability.Cves, + vulnerability.IssueId, + vulnerability.Severity, + vulnerability.Technology.GetPackageDescriptor(), + vulnerability.Components, + vulnerability.Applicable, + vulnerability.ImpactedDependencyName, + vulnerability.ImpactedDependencyVersion, + vulnerability.Summary, + vulnerability.FixedVersions, + markdownOutput, + run, + ); err != nil { + return err + } + } + for _, violation := range xrayJson.SecurityViolations { + if err := addXrayCveIssueToSarifRun( + violation.Cves, + violation.IssueId, + violation.Severity, + violation.Technology.GetPackageDescriptor(), + violation.Components, + violation.Applicable, + violation.ImpactedDependencyName, + violation.ImpactedDependencyVersion, + violation.Summary, + violation.FixedVersions, + markdownOutput, + run, + ); err != nil { + return err + } + } + for _, license := range xrayJson.LicensesViolations { + msg := getVulnerabilityOrViolationSarifHeadline(license.LicenseKey, license.ImpactedDependencyName, license.ImpactedDependencyVersion) + if rule, isNewRule := addResultToSarifRun(license.LicenseKey, msg, license.Severity, nil, run); isNewRule { + rule.WithDescription("License watch violations") + } + } + return nil +} + +func addXrayCveIssueToSarifRun(cves []formats.CveRow, issueId, severity, file string, components []formats.ComponentRow, applicable, impactedDependencyName, impactedDependencyVersion, summary string, fixedVersions []string, markdownOutput bool, run *sarif.Run) error { + maxCveScore, err := findMaxCVEScore(cves) + if err != nil { + return err + } + cveId := getCves(cves, issueId) + msg := getVulnerabilityOrViolationSarifHeadline(impactedDependencyName, impactedDependencyVersion, cveId) + location := sarif.NewLocation().WithPhysicalLocation(sarif.NewPhysicalLocation().WithArtifactLocation(sarif.NewArtifactLocation().WithUri(file))) + + if rule, isNewRule := addResultToSarifRun(cveId, msg, severity, location, run); isNewRule { + cveRuleProperties := sarif.NewPropertyBag() + if maxCveScore != MissingCveScore { + cveRuleProperties.Add("security-severity", maxCveScore) + } + rule.WithProperties(cveRuleProperties.Properties) + if markdownOutput { + formattedDirectDependencies, err := getDirectDependenciesFormatted(components) + if err != nil { + return err + } + markdownDescription := getSarifTableDescription(formattedDirectDependencies, maxCveScore, applicable, fixedVersions) + "\n" + rule.WithMarkdownHelp(markdownDescription) + } else { + rule.WithDescription(summary) + } + } + return nil +} + +func addResultToSarifRun(issueId, msg, severity string, location *sarif.Location, run *sarif.Run) (rule *sarif.ReportingDescriptor, isNewRule bool) { + if rule, _ = run.GetRuleById(issueId); rule == nil { + isNewRule = true + rule = run.AddRule(issueId) + } + if result := run.CreateResultForRule(issueId).WithMessage(sarif.NewTextMessage(msg)).WithLevel(ConvertToSarifLevel(severity)); location != nil { + result.AddLocation(location) + } + return +} + +func convertXrayScanToSimpleJson(extendedResults *ExtendedScanResults, isMultipleRoots, includeLicenses, simplifiedOutput bool) (formats.SimpleJsonResults, error) { violations, vulnerabilities, licenses := SplitScanResults(extendedResults.XrayResults) jsonTable := formats.SimpleJsonResults{} if len(vulnerabilities) > 0 { @@ -162,14 +294,6 @@ func convertScanToSimpleJson(extendedResults *ExtendedScanResults, errors []form jsonTable.LicensesViolations = licViolationsJsonTable jsonTable.OperationalRiskViolations = opRiskViolationsJsonTable } - if len(extendedResults.SecretsScanResults) > 0 { - secretsRows := PrepareSecrets(extendedResults.SecretsScanResults) - jsonTable.Secrets = secretsRows - } - if len(extendedResults.IacScanResults) > 0 { - iacRows := PrepareIacs(extendedResults.IacScanResults) - jsonTable.Iacs = iacRows - } if includeLicenses { licJsonTable, err := PrepareLicenses(licenses) if err != nil { @@ -177,82 +301,27 @@ func convertScanToSimpleJson(extendedResults *ExtendedScanResults, errors []form } jsonTable.Licenses = licJsonTable } - jsonTable.Errors = errors return jsonTable, nil } -func convertScanToSarif(run *sarif.Run, extendedResults *ExtendedScanResults, isMultipleRoots, markdownOutput bool) error { - var errors []formats.SimpleJsonError - jsonTable, err := convertScanToSimpleJson(extendedResults, errors, isMultipleRoots, true, markdownOutput) +func convertScanToSimpleJson(extendedResults *ExtendedScanResults, errors []formats.SimpleJsonError, isMultipleRoots, includeLicenses, simplifiedOutput bool) (formats.SimpleJsonResults, error) { + jsonTable, err := convertXrayScanToSimpleJson(extendedResults, isMultipleRoots, includeLicenses, simplifiedOutput) if err != nil { - return err + return formats.SimpleJsonResults{}, err } - if len(jsonTable.Vulnerabilities) > 0 || len(jsonTable.SecurityViolations) > 0 { - if err = convertToVulnerabilityOrViolationSarif(run, &jsonTable, markdownOutput); err != nil { - return err - } + if len(extendedResults.SecretsScanResults) > 0 { + jsonTable.Secrets = PrepareSecrets(extendedResults.SecretsScanResults) } - return convertToIacOrSecretsSarif(run, &jsonTable, markdownOutput) -} - -func convertToVulnerabilityOrViolationSarif(run *sarif.Run, jsonTable *formats.SimpleJsonResults, markdownOutput bool) error { - if len(jsonTable.SecurityViolations) > 0 { - return convertViolationsToSarif(jsonTable, run, markdownOutput) + if len(extendedResults.IacScanResults) > 0 { + jsonTable.Iacs = PrepareIacs(extendedResults.IacScanResults) } - return convertVulnerabilitiesToSarif(jsonTable, run, markdownOutput) -} - -func convertToIacOrSecretsSarif(run *sarif.Run, jsonTable *formats.SimpleJsonResults, markdownOutput bool) error { - var err error - for _, secret := range jsonTable.Secrets { - properties := getIacOrSecretsProperties(secret, markdownOutput, true) - if err = addPropertiesToSarifRun(run, &properties); err != nil { - return err - } + if len(extendedResults.SastScanResults) > 0 { + jsonTable.Sast = PrepareSast(extendedResults.SastScanResults) } + jsonTable.Errors = errors - for _, iac := range jsonTable.Iacs { - properties := getIacOrSecretsProperties(iac, markdownOutput, false) - if err = addPropertiesToSarifRun(run, &properties); err != nil { - return err - } - } - return err -} - -func getIacOrSecretsProperties(secretOrIac formats.IacSecretsRow, markdownOutput, isSecret bool) sarifProperties { - file := strings.TrimPrefix(secretOrIac.File, string(os.PathSeparator)) - mapSeverityToScore := map[string]string{ - "": "0.0", - "low": "3.9", - "medium": "6.9", - "high": "8.9", - "critical": "10", - } - severity := mapSeverityToScore[strings.ToLower(secretOrIac.Severity)] - markdownDescription := "" - headline := "Infrastructure as Code Vulnerability" - secretOrFinding := "Finding" - if isSecret { - secretOrFinding = "Secret" - headline = "Potential Secret Exposed" - } - if markdownOutput { - headerRow := fmt.Sprintf("| Severity | File | Line:Column | %s |\n", secretOrFinding) - separatorRow := "| :---: | :---: | :---: | :---: |\n" - tableHeader := headerRow + separatorRow - markdownDescription = tableHeader + fmt.Sprintf("| %s | %s | %s | %s |", secretOrIac.Severity, file, secretOrIac.LineColumn, secretOrIac.Text) - } - return sarifProperties{ - Headline: headline, - Severity: severity, - Description: secretOrIac.Text, - MarkdownDescription: markdownDescription, - File: file, - LineColumn: secretOrIac.LineColumn, - SecretsOrIacType: secretOrIac.Type, - } + return jsonTable, nil } func getCves(cvesRow []formats.CveRow, issueId string) string { @@ -275,68 +344,6 @@ func getVulnerabilityOrViolationSarifHeadline(depName, version, key string) stri return fmt.Sprintf("[%s] %s %s", key, depName, version) } -func convertViolationsToSarif(jsonTable *formats.SimpleJsonResults, run *sarif.Run, markdownOutput bool) error { - for _, violation := range jsonTable.SecurityViolations { - properties, err := getViolatedDepsSarifProps(violation, markdownOutput) - if err != nil { - return err - } - if err = addPropertiesToSarifRun(run, &properties); err != nil { - return err - } - } - for _, license := range jsonTable.LicensesViolations { - if err := addPropertiesToSarifRun(run, - &sarifProperties{ - Severity: license.Severity, - Headline: getVulnerabilityOrViolationSarifHeadline(license.LicenseKey, license.ImpactedDependencyName, license.ImpactedDependencyVersion)}); err != nil { - return err - } - } - - return nil -} - -func getViolatedDepsSarifProps(vulnerabilityRow formats.VulnerabilityOrViolationRow, markdownOutput bool) (sarifProperties, error) { - cves := getCves(vulnerabilityRow.Cves, vulnerabilityRow.IssueId) - headline := getVulnerabilityOrViolationSarifHeadline(vulnerabilityRow.ImpactedDependencyName, vulnerabilityRow.ImpactedDependencyVersion, cves) - maxCveScore, err := findMaxCVEScore(vulnerabilityRow.Cves) - if err != nil { - return sarifProperties{}, err - } - formattedDirectDependencies, err := getDirectDependenciesFormatted(vulnerabilityRow.Components) - if err != nil { - return sarifProperties{}, err - } - markdownDescription := "" - if markdownOutput { - markdownDescription = getSarifTableDescription(formattedDirectDependencies, maxCveScore, vulnerabilityRow.Applicable, vulnerabilityRow.FixedVersions) + "\n" - } - return sarifProperties{ - Applicable: vulnerabilityRow.Applicable, - Cves: cves, - Headline: headline, - Severity: maxCveScore, - Description: vulnerabilityRow.Summary, - MarkdownDescription: markdownDescription, - File: vulnerabilityRow.Technology.GetPackageDescriptor(), - }, err -} - -func convertVulnerabilitiesToSarif(jsonTable *formats.SimpleJsonResults, run *sarif.Run, simplifiedOutput bool) error { - for _, vulnerability := range jsonTable.Vulnerabilities { - properties, err := getViolatedDepsSarifProps(vulnerability, simplifiedOutput) - if err != nil { - return err - } - if err = addPropertiesToSarifRun(run, &properties); err != nil { - return err - } - } - - return nil -} - func getDirectDependenciesFormatted(directDependencies []formats.ComponentRow) (string, error) { var formattedDirectDependencies strings.Builder for _, dependency := range directDependencies { @@ -352,7 +359,7 @@ func getSarifTableDescription(formattedDirectDependencies, maxCveScore, applicab if len(fixedVersions) > 0 { descriptionFixVersions = strings.Join(fixedVersions, ", ") } - if applicable == "" { + if applicable == string(NotScanned) { return fmt.Sprintf("| Severity Score | Direct Dependencies | Fixed Versions |\n| :---: | :----: | :---: |\n| %s | %s | %s |", maxCveScore, formattedDirectDependencies, descriptionFixVersions) } @@ -360,60 +367,6 @@ func getSarifTableDescription(formattedDirectDependencies, maxCveScore, applicab maxCveScore, applicable, formattedDirectDependencies, descriptionFixVersions) } -// Adding the Xray scan results details to the sarif struct, for each issue found in the scan -func addPropertiesToSarifRun(run *sarif.Run, properties *sarifProperties) error { - pb := sarif.NewPropertyBag() - if properties.Severity != missingCveScore { - pb.Add("security-severity", properties.Severity) - } - description := properties.Description - markdownDescription := properties.MarkdownDescription - if markdownDescription != "" { - description = "" - } - line := 0 - column := 0 - var err error - if properties.LineColumn != "" { - lineColumn := strings.Split(properties.LineColumn, ":") - if line, err = strconv.Atoi(lineColumn[0]); err != nil { - return err - } - if column, err = strconv.Atoi(lineColumn[1]); err != nil { - return err - } - } - ruleID := generateSarifRuleID(properties) - run.AddRule(ruleID). - WithDescription(description). - WithProperties(pb.Properties). - WithMarkdownHelp(markdownDescription) - run.CreateResultForRule(ruleID). - WithMessage(sarif.NewTextMessage(properties.Headline)). - AddLocation( - sarif.NewLocationWithPhysicalLocation( - sarif.NewPhysicalLocation(). - WithArtifactLocation( - sarif.NewSimpleArtifactLocation(properties.File), - ).WithRegion( - sarif.NewSimpleRegion(line, line). - WithStartColumn(column)), - ), - ) - return nil -} - -func generateSarifRuleID(properties *sarifProperties) string { - switch { - case properties.Cves != "": - return properties.Cves - case properties.XrayID != "": - return properties.XrayID - default: - return properties.File - } -} - func findMaxCVEScore(cves []formats.CveRow) (string, error) { maxCve := 0.0 for _, cve := range cves { diff --git a/xray/utils/resultwriter_test.go b/xray/utils/resultwriter_test.go index 44deddaba..6f13d6a03 100644 --- a/xray/utils/resultwriter_test.go +++ b/xray/utils/resultwriter_test.go @@ -1,87 +1,12 @@ package utils import ( - "fmt" - "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "testing" + "github.com/jfrog/jfrog-cli-core/v2/xray/formats" - "github.com/jfrog/jfrog-client-go/xray/services" "github.com/stretchr/testify/assert" - "path" - "testing" ) -func TestGenerateSarifFileFromScan(t *testing.T) { - extendedResults := &ExtendedScanResults{ - XrayResults: []services.ScanResponse{ - { - Vulnerabilities: []services.Vulnerability{ - { - Cves: []services.Cve{{Id: "CVE-2022-1234", CvssV3Score: "8.0"}, {Id: "CVE-2023-1234", CvssV3Score: "7.1"}}, - Summary: "A test vulnerability the harms nothing", - Severity: "High", - Components: map[string]services.Component{ - "vulnerability1": {FixedVersions: []string{"1.2.3"}}, - }, - Technology: coreutils.Go.ToString(), - }, - }, - }, - }, - SecretsScanResults: []IacOrSecretResult{ - { - Severity: "Medium", - File: "found_secrets.js", - LineColumn: "1:18", - Type: "entropy", - Text: "AAA************", - }, - }, - IacScanResults: []IacOrSecretResult{ - { - Severity: "Medium", - File: "plan/nonapplicable/req_sw_terraform_azure_compute_no_pass_auth.json", - LineColumn: "229:38", - Type: "entropy", - Text: "BBB************", - }, - }, - } - testCases := []struct { - name string - extendedResults *ExtendedScanResults - isMultipleRoots bool - markdownOutput bool - expectedSarifOutput string - }{ - { - name: "Scan results with vulnerabilities, secrets and IaC", - extendedResults: extendedResults, - expectedSarifOutput: "{\n \"version\": \"2.1.0\",\n \"$schema\": \"https://json.schemastore.org/sarif-2.1.0.json\",\n \"runs\": [\n {\n \"tool\": {\n \"driver\": {\n \"informationUri\": \"https://example.com/\",\n \"name\": \"JFrog Security\",\n \"rules\": [\n {\n \"id\": \"CVE-2022-1234, CVE-2023-1234\",\n \"shortDescription\": {\n \"text\": \"A test vulnerability the harms nothing\"\n },\n \"help\": {\n \"markdown\": \"\"\n },\n \"properties\": {\n \"security-severity\": \"8.0\"\n }\n },\n {\n \"id\": \"found_secrets.js\",\n \"shortDescription\": {\n \"text\": \"AAA************\"\n },\n \"help\": {\n \"markdown\": \"\"\n },\n \"properties\": {\n \"security-severity\": \"6.9\"\n }\n },\n {\n \"id\": \"plan/nonapplicable/req_sw_terraform_azure_compute_no_pass_auth.json\",\n \"shortDescription\": {\n \"text\": \"BBB************\"\n },\n \"help\": {\n \"markdown\": \"\"\n },\n \"properties\": {\n \"security-severity\": \"6.9\"\n }\n }\n ]\n }\n },\n \"results\": [\n {\n \"ruleId\": \"CVE-2022-1234, CVE-2023-1234\",\n \"ruleIndex\": 0,\n \"message\": {\n \"text\": \"[CVE-2022-1234, CVE-2023-1234] vulnerability1 \"\n },\n \"locations\": [\n {\n \"physicalLocation\": {\n \"artifactLocation\": {\n \"uri\": \"go.mod\"\n },\n \"region\": {\n \"startLine\": 0,\n \"startColumn\": 0,\n \"endLine\": 0\n }\n }\n }\n ]\n },\n {\n \"ruleId\": \"found_secrets.js\",\n \"ruleIndex\": 1,\n \"message\": {\n \"text\": \"Potential Secret Exposed\"\n },\n \"locations\": [\n {\n \"physicalLocation\": {\n \"artifactLocation\": {\n \"uri\": \"found_secrets.js\"\n },\n \"region\": {\n \"startLine\": 1,\n \"startColumn\": 18,\n \"endLine\": 1\n }\n }\n }\n ]\n },\n {\n \"ruleId\": \"plan/nonapplicable/req_sw_terraform_azure_compute_no_pass_auth.json\",\n \"ruleIndex\": 2,\n \"message\": {\n \"text\": \"Infrastructure as Code Vulnerability\"\n },\n \"locations\": [\n {\n \"physicalLocation\": {\n \"artifactLocation\": {\n \"uri\": \"plan/nonapplicable/req_sw_terraform_azure_compute_no_pass_auth.json\"\n },\n \"region\": {\n \"startLine\": 229,\n \"startColumn\": 38,\n \"endLine\": 229\n }\n }\n }\n ]\n }\n ]\n }\n ]\n}", - }, - { - name: "Scan results with vulnerabilities, secrets and IaC as Markdown", - extendedResults: extendedResults, - markdownOutput: true, - expectedSarifOutput: "{\n \"version\": \"2.1.0\",\n \"$schema\": \"https://json.schemastore.org/sarif-2.1.0.json\",\n \"runs\": [\n {\n \"tool\": {\n \"driver\": {\n \"informationUri\": \"https://example.com/\",\n \"name\": \"JFrog Security\",\n \"rules\": [\n {\n \"id\": \"CVE-2022-1234, CVE-2023-1234\",\n \"shortDescription\": {\n \"text\": \"\"\n },\n \"help\": {\n \"markdown\": \"| Severity Score | Direct Dependencies | Fixed Versions |\\n| :---: | :----: | :---: |\\n| 8.0 | | 1.2.3 |\\n\"\n },\n \"properties\": {\n \"security-severity\": \"8.0\"\n }\n },\n {\n \"id\": \"found_secrets.js\",\n \"shortDescription\": {\n \"text\": \"\"\n },\n \"help\": {\n \"markdown\": \"| Severity | File | Line:Column | Secret |\\n| :---: | :---: | :---: | :---: |\\n| Medium | found_secrets.js | 1:18 | AAA************ |\"\n },\n \"properties\": {\n \"security-severity\": \"6.9\"\n }\n },\n {\n \"id\": \"plan/nonapplicable/req_sw_terraform_azure_compute_no_pass_auth.json\",\n \"shortDescription\": {\n \"text\": \"\"\n },\n \"help\": {\n \"markdown\": \"| Severity | File | Line:Column | Finding |\\n| :---: | :---: | :---: | :---: |\\n| Medium | plan/nonapplicable/req_sw_terraform_azure_compute_no_pass_auth.json | 229:38 | BBB************ |\"\n },\n \"properties\": {\n \"security-severity\": \"6.9\"\n }\n }\n ]\n }\n },\n \"results\": [\n {\n \"ruleId\": \"CVE-2022-1234, CVE-2023-1234\",\n \"ruleIndex\": 0,\n \"message\": {\n \"text\": \"[CVE-2022-1234, CVE-2023-1234] vulnerability1 \"\n },\n \"locations\": [\n {\n \"physicalLocation\": {\n \"artifactLocation\": {\n \"uri\": \"go.mod\"\n },\n \"region\": {\n \"startLine\": 0,\n \"startColumn\": 0,\n \"endLine\": 0\n }\n }\n }\n ]\n },\n {\n \"ruleId\": \"found_secrets.js\",\n \"ruleIndex\": 1,\n \"message\": {\n \"text\": \"Potential Secret Exposed\"\n },\n \"locations\": [\n {\n \"physicalLocation\": {\n \"artifactLocation\": {\n \"uri\": \"found_secrets.js\"\n },\n \"region\": {\n \"startLine\": 1,\n \"startColumn\": 18,\n \"endLine\": 1\n }\n }\n }\n ]\n },\n {\n \"ruleId\": \"plan/nonapplicable/req_sw_terraform_azure_compute_no_pass_auth.json\",\n \"ruleIndex\": 2,\n \"message\": {\n \"text\": \"Infrastructure as Code Vulnerability\"\n },\n \"locations\": [\n {\n \"physicalLocation\": {\n \"artifactLocation\": {\n \"uri\": \"plan/nonapplicable/req_sw_terraform_azure_compute_no_pass_auth.json\"\n },\n \"region\": {\n \"startLine\": 229,\n \"startColumn\": 38,\n \"endLine\": 229\n }\n }\n }\n ]\n }\n ]\n }\n ]\n}", - }, - { - name: "Scan results without vulnerabilities", - extendedResults: &ExtendedScanResults{}, - isMultipleRoots: true, - markdownOutput: true, - expectedSarifOutput: "{\n \"version\": \"2.1.0\",\n \"$schema\": \"https://json.schemastore.org/sarif-2.1.0.json\",\n \"runs\": [\n {\n \"tool\": {\n \"driver\": {\n \"informationUri\": \"https://example.com/\",\n \"name\": \"JFrog Security\",\n \"rules\": []\n }\n },\n \"results\": []\n }\n ]\n}", - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - sarifOutput, err := GenerateSarifFileFromScan(testCase.extendedResults, testCase.isMultipleRoots, testCase.markdownOutput, "JFrog Security", "https://example.com/") - assert.NoError(t, err) - assert.Equal(t, testCase.expectedSarifOutput, sarifOutput) - }) - } -} - func TestGetVulnerabilityOrViolationSarifHeadline(t *testing.T) { assert.Equal(t, "[CVE-2022-1234] loadsh 1.4.1", getVulnerabilityOrViolationSarifHeadline("loadsh", "1.4.1", "CVE-2022-1234")) assert.NotEqual(t, "[CVE-2022-1234] loadsh 1.4.1", getVulnerabilityOrViolationSarifHeadline("loadsh", "1.2.1", "CVE-2022-1234")) @@ -96,162 +21,6 @@ func TestGetCves(t *testing.T) { assert.Equal(t, issueId, getCves(nil, issueId)) } -func TestGetIacOrSecretsProperties(t *testing.T) { - testCases := []struct { - name string - secretOrIac formats.IacSecretsRow - markdownOutput bool - isSecret bool - expectedOutput sarifProperties - }{ - { - name: "Infrastructure as Code vulnerability without markdown output", - secretOrIac: formats.IacSecretsRow{ - Severity: "high", - File: path.Join("path", "to", "file"), - LineColumn: "10:5", - Text: "Vulnerable code", - Type: "Terraform", - }, - markdownOutput: false, - isSecret: false, - expectedOutput: sarifProperties{ - Applicable: "", - Cves: "", - Headline: "Infrastructure as Code Vulnerability", - Severity: "8.9", - Description: "Vulnerable code", - MarkdownDescription: "", - XrayID: "", - File: path.Join("path", "to", "file"), - LineColumn: "10:5", - SecretsOrIacType: "Terraform", - }, - }, - { - name: "Potential secret exposed with markdown output", - secretOrIac: formats.IacSecretsRow{ - Severity: "medium", - File: path.Join("path", "to", "file"), - LineColumn: "5:3", - Text: "Potential secret", - Type: "AWS Secret Manager", - }, - markdownOutput: true, - isSecret: true, - expectedOutput: sarifProperties{ - Applicable: "", - Cves: "", - Headline: "Potential Secret Exposed", - Severity: "6.9", - Description: "Potential secret", - MarkdownDescription: fmt.Sprintf("| Severity | File | Line:Column | Secret |\n| :---: | :---: | :---: | :---: |\n| medium | %s | 5:3 | Potential secret |", path.Join("path", "to", "file")), - XrayID: "", - File: path.Join("path", "to", "file"), - LineColumn: "5:3", - SecretsOrIacType: "AWS Secret Manager", - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - output := getIacOrSecretsProperties(testCase.secretOrIac, testCase.markdownOutput, testCase.isSecret) - assert.Equal(t, testCase.expectedOutput.Applicable, output.Applicable) - assert.Equal(t, testCase.expectedOutput.Cves, output.Cves) - assert.Equal(t, testCase.expectedOutput.Headline, output.Headline) - assert.Equal(t, testCase.expectedOutput.Severity, output.Severity) - assert.Equal(t, testCase.expectedOutput.Description, output.Description) - assert.Equal(t, testCase.expectedOutput.MarkdownDescription, output.MarkdownDescription) - assert.Equal(t, testCase.expectedOutput.XrayID, output.XrayID) - assert.Equal(t, testCase.expectedOutput.File, output.File) - assert.Equal(t, testCase.expectedOutput.LineColumn, output.LineColumn) - assert.Equal(t, testCase.expectedOutput.SecretsOrIacType, output.SecretsOrIacType) - }) - } -} - -func TestGetViolatedDepsSarifProps(t *testing.T) { - testCases := []struct { - name string - vulnerability formats.VulnerabilityOrViolationRow - markdownOutput bool - expectedOutput sarifProperties - }{ - { - name: "Vulnerability with markdown output", - vulnerability: formats.VulnerabilityOrViolationRow{ - Summary: "Vulnerable dependency", - Severity: "high", - Applicable: "Applicable", - ImpactedDependencyName: "example-package", - ImpactedDependencyVersion: "1.0.0", - ImpactedDependencyType: "npm", - FixedVersions: []string{"1.0.1", "1.0.2"}, - Components: []formats.ComponentRow{ - {Name: "example-package", Version: "1.0.0"}, - }, - Cves: []formats.CveRow{ - {Id: "CVE-2021-1234", CvssV3: "7.2"}, - {Id: "CVE-2021-5678", CvssV3: "7.2"}, - }, - IssueId: "XRAY-12345", - }, - markdownOutput: true, - expectedOutput: sarifProperties{ - Applicable: "Applicable", - Cves: "CVE-2021-1234, CVE-2021-5678", - Headline: "[CVE-2021-1234, CVE-2021-5678] example-package 1.0.0", - Severity: "7.2", - Description: "Vulnerable dependency", - MarkdownDescription: "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 7.2 | Applicable | `example-package 1.0.0` | 1.0.1, 1.0.2 |\n", - }, - }, - { - name: "Vulnerability without markdown output", - vulnerability: formats.VulnerabilityOrViolationRow{ - Summary: "Vulnerable dependency", - Severity: "high", - Applicable: "Applicable", - ImpactedDependencyName: "example-package", - ImpactedDependencyVersion: "1.0.0", - ImpactedDependencyType: "npm", - FixedVersions: []string{"1.0.1", "1.0.2"}, - Components: []formats.ComponentRow{ - {Name: "example-package", Version: "1.0.0"}, - }, - Cves: []formats.CveRow{ - {Id: "CVE-2021-1234", CvssV3: "7.2"}, - {Id: "CVE-2021-5678", CvssV3: "7.2"}, - }, - IssueId: "XRAY-12345", - }, - expectedOutput: sarifProperties{ - Applicable: "Applicable", - Cves: "CVE-2021-1234, CVE-2021-5678", - Headline: "[CVE-2021-1234, CVE-2021-5678] example-package 1.0.0", - Severity: "7.2", - Description: "Vulnerable dependency", - MarkdownDescription: "", - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - output, err := getViolatedDepsSarifProps(tc.vulnerability, tc.markdownOutput) - assert.NoError(t, err) - assert.Equal(t, tc.expectedOutput.Cves, output.Cves) - assert.Equal(t, tc.expectedOutput.Severity, output.Severity) - assert.Equal(t, tc.expectedOutput.XrayID, output.XrayID) - assert.Equal(t, tc.expectedOutput.MarkdownDescription, output.MarkdownDescription) - assert.Equal(t, tc.expectedOutput.Applicable, output.Applicable) - assert.Equal(t, tc.expectedOutput.Description, output.Description) - assert.Equal(t, tc.expectedOutput.Headline, output.Headline) - }) - } -} - func TestGetDirectDependenciesFormatted(t *testing.T) { testCases := []struct { name string diff --git a/xray/utils/sarifutils.go b/xray/utils/sarifutils.go new file mode 100644 index 000000000..420243441 --- /dev/null +++ b/xray/utils/sarifutils.go @@ -0,0 +1,395 @@ +package utils + +import ( + "fmt" + "strconv" + "strings" + + "github.com/jfrog/gofrog/datastructures" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + "github.com/owenrumney/go-sarif/v2/sarif" +) + +type SarifLevel string + +const ( + errorLevel SarifLevel = "error" + warningLevel SarifLevel = "warning" + infoLevel SarifLevel = "info" + noteLevel SarifLevel = "note" + noneLevel SarifLevel = "none" + + SeverityDefaultValue = "Medium" +) + +var ( + // All other values (include default) mapped as 'Medium' severity + levelToSeverity = map[SarifLevel]string{ + errorLevel: "High", + noteLevel: "Low", + noneLevel: "Unknown", + } + + severityToLevel = map[string]SarifLevel{ + "critical": errorLevel, + "high": errorLevel, + "medium": warningLevel, + "low": noteLevel, + "unknown": noneLevel, + } +) + +func NewReport() (*sarif.Report, error) { + report, err := sarif.New(sarif.Version210) + if err != nil { + return nil, errorutils.CheckError(err) + } + return report, nil +} + +func ReadScanRunsFromFile(fileName string) (sarifRuns []*sarif.Run, err error) { + report, err := sarif.Open(fileName) + if errorutils.CheckError(err) != nil { + err = fmt.Errorf("can't read valid Sarif run from " + fileName + ": " + err.Error()) + return + } + sarifRuns = report.Runs + return +} + +func AggregateMultipleRunsIntoSingle(runs []*sarif.Run, destination *sarif.Run) { + if len(runs) == 0 { + return + } + for _, run := range runs { + if run == nil || len(run.Results) == 0 { + continue + } + for _, rule := range GetRunRules(run) { + if destination.Tool.Driver != nil { + destination.Tool.Driver.AddRule(rule) + } + } + for _, result := range run.Results { + destination.AddResult(result) + } + for _, invocation := range run.Invocations { + destination.AddInvocations(invocation) + } + } +} + +func getRunInformationUri(run *sarif.Run) string { + if run != nil && run.Tool.Driver != nil && run.Tool.Driver.InformationURI != nil { + return *run.Tool.Driver.InformationURI + } + return "" +} + +// Calculate new information that exists at the run and not at the source +func GetDiffFromRun(sources []*sarif.Run, targets []*sarif.Run) (runWithNewOnly *sarif.Run) { + // Combine + combinedSource := sarif.NewRunWithInformationURI(sources[0].Tool.Driver.Name, getRunInformationUri(sources[0])) + AggregateMultipleRunsIntoSingle(sources, combinedSource) + if combinedSource == nil { + return + } + combinedTarget := sarif.NewRunWithInformationURI(targets[0].Tool.Driver.Name, getRunInformationUri(targets[0])) + AggregateMultipleRunsIntoSingle(targets, combinedTarget) + if combinedTarget == nil { + return combinedSource + } + // Get diff + runWithNewOnly = sarif.NewRun(combinedSource.Tool).WithInvocations(combinedSource.Invocations) + for _, sourceResult := range combinedSource.Results { + targetMatchingResults := GetResultsByRuleId(combinedTarget, *sourceResult.RuleID) + if len(targetMatchingResults) == 0 { + runWithNewOnly.AddResult(sourceResult) + if rule, _ := combinedSource.GetRuleById(*sourceResult.RuleID); rule != nil { + runWithNewOnly.Tool.Driver.AddRule(rule) + } + continue + } + for _, targetMatchingResult := range targetMatchingResults { + if len(sourceResult.Locations) > len(targetMatchingResult.Locations) || + len(sourceResult.CodeFlows) > len(targetMatchingResult.CodeFlows) { + runWithNewOnly.AddResult(sourceResult) + if rule, _ := combinedSource.GetRuleById(*sourceResult.RuleID); rule != nil { + runWithNewOnly.Tool.Driver.AddRule(rule) + } + } + } + } + return +} + +// Calculate new information that exists at the result and not at the source +func GetDiffFromResult(result *sarif.Result, source *sarif.Result) *sarif.Result { + newLocations := datastructures.MakeSet[*sarif.Location]() + newCodeFlows := []*sarif.CodeFlow{} + for _, targetLocation := range result.Locations { + if !IsLocationInResult(targetLocation, source) { + newLocations.Add(targetLocation) + newCodeFlows = append(newCodeFlows, GetLocationRelatedCodeFlowsFromResult(targetLocation, result)...) + continue + } + // Location in result, compare related code flows + for _, targetCodeFlow := range GetLocationRelatedCodeFlowsFromResult(targetLocation, result) { + for _, sourceCodeFlow := range GetLocationRelatedCodeFlowsFromResult(targetLocation, source) { + if !IsSameCodeFlow(targetCodeFlow, sourceCodeFlow) { + // Code flow does not exists at source, add it and it's related location + newLocations.Add(targetLocation) + newCodeFlows = append(newCodeFlows, targetCodeFlow) + } + } + } + } + // Create the result only with new information + return sarif.NewRuleResult(*result.RuleID). + WithKind(*result.Kind). + WithMessage(&result.Message). + WithLevel(*result.Level). + WithLocations(newLocations.ToSlice()). + WithCodeFlows(newCodeFlows) +} + +func FilterResultsByRuleIdAndMsgText(source []*sarif.Result, ruleId, msgText string) (results []*sarif.Result) { + for _, result := range source { + if ruleId == *result.RuleID && msgText == GetResultMsgText(result) { + results = append(results, result) + } + } + return +} + +func GetLocationRelatedCodeFlowsFromResult(location *sarif.Location, result *sarif.Result) (codeFlows []*sarif.CodeFlow) { + for _, codeFlow := range result.CodeFlows { + for _, stackTrace := range codeFlow.ThreadFlows { + // The threadFlow is reverse stack trace. + // The last location is the location that it relates to. + if IsSameLocation(location, stackTrace.Locations[len(stackTrace.Locations)-1].Location) { + codeFlows = append(codeFlows, codeFlow) + } + } + } + return +} + +func IsSameCodeFlow(codeFlow *sarif.CodeFlow, other *sarif.CodeFlow) bool { + if len(codeFlow.ThreadFlows) != len(other.ThreadFlows) { + return false + } + // ThreadFlows is unordered list of stack trace + for _, stackTrace := range codeFlow.ThreadFlows { + foundMatch := false + for _, otherStackTrace := range other.ThreadFlows { + if len(stackTrace.Locations) != len(otherStackTrace.Locations) { + continue + } + for i, stackTraceLocation := range stackTrace.Locations { + if !IsSameLocation(stackTraceLocation.Location, otherStackTrace.Locations[i].Location) { + continue + } + } + foundMatch = true + } + if !foundMatch { + return false + } + } + return true +} + +func IsLocationInResult(location *sarif.Location, result *sarif.Result) bool { + for _, resultLocation := range result.Locations { + if IsSameLocation(location, resultLocation) { + return true + } + } + return false +} + +func IsSameLocation(location *sarif.Location, other *sarif.Location) bool { + if location == other { + return true + } + return GetLocationFileName(location) == GetLocationFileName(other) && + GetLocationSnippet(location) == GetLocationSnippet(other) && + GetLocationStartLine(location) == GetLocationStartLine(other) && + GetLocationStartColumn(location) == GetLocationStartColumn(other) && + GetLocationEndLine(location) == GetLocationEndLine(other) && + GetLocationEndColumn(location) == GetLocationEndColumn(other) +} + +func GetResultsLocationCount(runs ...*sarif.Run) (count int) { + for _, run := range runs { + for _, result := range run.Results { + count += len(result.Locations) + } + } + return +} + +func GetLevelResultsLocationCount(run *sarif.Run, level SarifLevel) (count int) { + for _, result := range run.Results { + if level == SarifLevel(*result.Level) { + count += len(result.Locations) + } + } + return +} + +func GetResultsByRuleId(run *sarif.Run, ruleId string) (results []*sarif.Result) { + for _, result := range run.Results { + if *result.RuleID == ruleId { + results = append(results, result) + } + } + return +} + +func GetResultMsgText(result *sarif.Result) string { + if result.Message.Text != nil { + return *result.Message.Text + } + return "" +} + +func GetLocationSnippet(location *sarif.Location) string { + snippet := GetLocationSnippetPointer(location) + if snippet == nil { + return "" + } + return *snippet +} + +func GetLocationSnippetPointer(location *sarif.Location) *string { + region := getLocationRegion(location) + if region != nil && region.Snippet != nil { + return region.Snippet.Text + } + return nil +} + +func SetLocationSnippet(location *sarif.Location, snippet string) { + if location != nil && location.PhysicalLocation != nil && location.PhysicalLocation.Region != nil && location.PhysicalLocation.Region.Snippet != nil { + location.PhysicalLocation.Region.Snippet.Text = &snippet + } +} + +func GetLocationFileName(location *sarif.Location) string { + filePath := location.PhysicalLocation.ArtifactLocation.URI + if filePath != nil { + return *filePath + } + return "" +} + +func SetLocationFileName(location *sarif.Location, fileName string) { + if location != nil && location.PhysicalLocation != nil && location.PhysicalLocation.Region != nil && location.PhysicalLocation.Region.Snippet != nil { + location.PhysicalLocation.ArtifactLocation.URI = &fileName + } +} + +func getLocationRegion(location *sarif.Location) *sarif.Region { + if location != nil && location.PhysicalLocation != nil { + return location.PhysicalLocation.Region + } + return nil +} + +func GetLocationStartLine(location *sarif.Location) int { + region := getLocationRegion(location) + if region != nil && region.StartLine != nil { + return *region.StartLine + } + return 0 +} + +func GetLocationStartColumn(location *sarif.Location) int { + region := getLocationRegion(location) + if region != nil && region.StartColumn != nil { + return *region.StartColumn + } + return 0 +} + +func GetLocationEndLine(location *sarif.Location) int { + region := getLocationRegion(location) + if region != nil && region.EndLine != nil { + return *region.EndLine + } + return 0 +} + +func GetLocationEndColumn(location *sarif.Location) int { + region := getLocationRegion(location) + if region != nil && region.EndColumn != nil { + return *region.EndColumn + } + return 0 +} + +func GetStartLocationInFile(location *sarif.Location) string { + startLine := location.PhysicalLocation.Region.StartLine + startColumn := location.PhysicalLocation.Region.StartColumn + if startLine != nil && startColumn != nil { + return strconv.Itoa(*startLine) + ":" + strconv.Itoa(*startColumn) + } + return "" +} + +func ExtractRelativePath(resultPath string, projectRoot string) string { + filePrefix := "file://" + relativePath := strings.ReplaceAll(strings.ReplaceAll(resultPath, projectRoot, ""), filePrefix, "") + return relativePath +} + +func GetResultSeverity(result *sarif.Result) string { + if result.Level != nil { + if severity, ok := levelToSeverity[SarifLevel(strings.ToLower(*result.Level))]; ok { + return severity + } + } + return SeverityDefaultValue +} + +func ConvertToSarifLevel(severity string) string { + if level, ok := severityToLevel[strings.ToLower(severity)]; ok { + return string(level) + } + return string(noneLevel) +} + +func isApplicableResult(result *sarif.Result) bool { + return !(result.Kind != nil && *result.Kind == "pass") +} + +func GetRuleFullDescription(rule *sarif.ReportingDescriptor) string { + if rule.FullDescription != nil && rule.FullDescription.Text != nil { + return *rule.FullDescription.Text + } + return "" +} + +func GetRuleIdFromCveId(cveId string) string { + return "applic_" + cveId +} + +func GetCveIdFromRuleId(sarifRuleId string) string { + return strings.TrimPrefix(sarifRuleId, "applic_") +} + +func GetRunRules(run *sarif.Run) []*sarif.ReportingDescriptor { + if run != nil && run.Tool.Driver != nil { + return run.Tool.Driver.Rules + } + return []*sarif.ReportingDescriptor{} +} + +func GetInvocationWorkingDirectory(invocation *sarif.Invocation) string { + if invocation.WorkingDirectory != nil && invocation.WorkingDirectory.URI != nil { + return *invocation.WorkingDirectory.URI + } + return "" +} diff --git a/xray/utils/sarifutils_test.go b/xray/utils/sarifutils_test.go new file mode 100644 index 000000000..4e0031268 --- /dev/null +++ b/xray/utils/sarifutils_test.go @@ -0,0 +1,43 @@ +package utils + +import ( + "github.com/jfrog/gofrog/datastructures" + "github.com/owenrumney/go-sarif/v2/sarif" +) + +func getRunWithDummyResults(results ...*sarif.Result) *sarif.Run { + run := sarif.NewRunWithInformationURI("", "") + ids := datastructures.MakeSet[string]() + for _, result := range results { + if !ids.Exists(*result.RuleID) { + run.Tool.Driver.Rules = append(run.Tool.Driver.Rules, sarif.NewRule(*result.RuleID)) + ids.Add(*result.RuleID) + } + } + return run.WithResults(results) +} + +func getDummyPassingResult(ruleId string) *sarif.Result { + kind := "pass" + return &sarif.Result{ + Kind: &kind, + RuleID: &ruleId, + } +} + +func getDummyResultWithOneLocation(fileName string, startLine, startCol int, snippet, ruleId string, level string) *sarif.Result { + return &sarif.Result{ + Locations: []*sarif.Location{ + { + PhysicalLocation: &sarif.PhysicalLocation{ + ArtifactLocation: &sarif.ArtifactLocation{URI: &fileName}, + Region: &sarif.Region{ + StartLine: &startLine, + StartColumn: &startCol, + Snippet: &sarif.ArtifactContent{Text: &snippet}}}, + }, + }, + Level: &level, + RuleID: &ruleId, + } +} diff --git a/xray/utils/xraymanager.go b/xray/utils/xraymanager.go new file mode 100644 index 000000000..eb7446a2e --- /dev/null +++ b/xray/utils/xraymanager.go @@ -0,0 +1,33 @@ +package utils + +import ( + "github.com/jfrog/jfrog-cli-core/v2/utils/config" + clientconfig "github.com/jfrog/jfrog-client-go/config" + "github.com/jfrog/jfrog-client-go/xray" +) + +func CreateXrayServiceManager(serviceDetails *config.ServerDetails) (*xray.XrayServicesManager, error) { + xrayDetails, err := serviceDetails.CreateXrayAuthConfig() + if err != nil { + return nil, err + } + serviceConfig, err := clientconfig.NewConfigBuilder(). + SetServiceDetails(xrayDetails). + Build() + if err != nil { + return nil, err + } + return xray.New(serviceConfig) +} + +func CreateXrayServiceManagerAndGetVersion(serviceDetails *config.ServerDetails) (*xray.XrayServicesManager, string, error) { + xrayManager, err := CreateXrayServiceManager(serviceDetails) + if err != nil { + return nil, "", err + } + xrayVersion, err := xrayManager.GetVersion() + if err != nil { + return nil, "", err + } + return xrayManager, xrayVersion, nil +}