diff --git a/commands/audit/sca/nuget/nuget.go b/commands/audit/sca/nuget/nuget.go index d34c4e47..ffd76630 100644 --- a/commands/audit/sca/nuget/nuget.go +++ b/commands/audit/sca/nuget/nuget.go @@ -40,7 +40,7 @@ const ( globalPackagesNotFoundErrorMessage = "could not find global packages path at:" ) -// BuildDependencyTree generates a temporary duplicate of the project to execute the 'install' command without impacting the original directory and establishing the JFrog configuration file for Artifactory resolution +// Generates a temporary duplicate of the project to execute the 'install' command without impacting the original directory and establishing the JFrog configuration file for Artifactory resolution // Additionally, re-loads the project's Solution so the dependencies sources will be identified func BuildDependencyTree(params utils.AuditParams) (dependencyTree []*xrayUtils.GraphNode, uniqueDeps []string, err error) { wd, err := os.Getwd() @@ -55,8 +55,28 @@ func BuildDependencyTree(params utils.AuditParams) (dependencyTree []*xrayUtils. return } - // Creating a temporary copy of the project in order to run 'install' command without effecting the original directory + creating the jfrog config for artifactory resolution - tmpWd, err := fileutils.CreateTempDir() + installRequired, err := isInstallRequired(params, sol, params.SkipAutoInstall(), wd) + if err != nil { + return + } + + var buildInfo *entities.BuildInfo + if installRequired { + buildInfo, err = restoreInTempDirAndGetBuildInfo(params, wd, exclusionPattern) + } else { + buildInfo, err = sol.BuildInfo("", log.Logger) + } + + if err != nil { + return + } + dependencyTree, uniqueDeps = parseNugetDependencyTree(buildInfo) + return +} + +func restoreInTempDirAndGetBuildInfo(params utils.AuditParams, wd string, exclusionPattern string) (buildInfo *entities.BuildInfo, err error) { + var tmpWd string + tmpWd, err = fileutils.CreateTempDir() if err != nil { err = fmt.Errorf("failed to create a temporary dir: %w", err) return @@ -65,36 +85,35 @@ func BuildDependencyTree(params utils.AuditParams) (dependencyTree []*xrayUtils. err = errors.Join(err, fileutils.RemoveTempDir(tmpWd)) }() - // Exclude Visual Studio inner directorty since it is not neccessary for the scan process and may cause race condition. + // Exclude Visual Studio inner directory since it is not necessary for the scan process and may cause race condition. err = biutils.CopyDir(wd, tmpWd, true, []string{sca.DotVsRepoSuffix}) if err != nil { err = fmt.Errorf("failed copying project to temp dir: %w", err) return } - if isInstallRequired(params, sol) { - log.Info("Dependencies sources were not detected nor 'install' command provided. Running 'restore' command") - sol, err = runDotnetRestoreAndLoadSolution(params, tmpWd, exclusionPattern) - if err != nil { - return - } - } - - buildInfo, err := sol.BuildInfo("", log.Logger) + log.Info("Dependencies sources were not detected nor 'install' command provided. Running 'restore' command") + sol, err := runDotnetRestoreAndLoadSolution(params, tmpWd, exclusionPattern) if err != nil { return } - dependencyTree, uniqueDeps = parseNugetDependencyTree(buildInfo) - return + return sol.BuildInfo("", log.Logger) } // Verifies whether the execution of an 'install' command is necessary, either because the project isn't installed or because the user has specified an 'install' command -func isInstallRequired(params utils.AuditParams, sol solution.Solution) bool { +func isInstallRequired(params utils.AuditParams, sol solution.Solution, skipAutoInstall bool, curWd string) (bool, error) { // If the user has specified an 'install' command, we proceed with executing the 'restore' command even if the project is already installed // Additionally, if dependency sources were not identified during the construction of the Solution struct, the project will necessitate an 'install' solDependencySourcesExists := len(sol.GetDependenciesSources()) > 0 solProjectsExists := len(sol.GetProjects()) > 0 - return len(params.InstallCommandArgs()) > 0 || !solDependencySourcesExists || !solProjectsExists || params.IsCurationCmd() + installRequired := !solDependencySourcesExists || !solProjectsExists || params.IsCurationCmd() + + if len(params.InstallCommandArgs()) > 0 { + return true, nil + } else if installRequired && skipAutoInstall { + return false, &biutils.ErrProjectNotInstalled{UninstalledDir: curWd} + } + return installRequired, nil } func runDotnetRestoreAndLoadSolution(params utils.AuditParams, tmpWd, exclusionPattern string) (sol solution.Solution, err error) { diff --git a/commands/audit/sca/nuget/nuget_test.go b/commands/audit/sca/nuget/nuget_test.go index 0ab6aba9..4b0b0325 100644 --- a/commands/audit/sca/nuget/nuget_test.go +++ b/commands/audit/sca/nuget/nuget_test.go @@ -10,6 +10,7 @@ import ( xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" "os" "path/filepath" + "strings" "testing" "github.com/jfrog/build-info-go/entities" @@ -146,3 +147,77 @@ func TestRunDotnetRestoreAndLoadSolution(t *testing.T) { assert.NotEmpty(t, sol.GetDependenciesSources()) } } + +// This test checks that the tree construction is skipped when the project is not installed and the user prohibited installation +func TestSkipBuildDepTreeWhenInstallForbidden(t *testing.T) { + testCases := []struct { + name string + testDir string + installCommand string + successfulTreeBuiltExpected bool + }{ + { + name: "nuget single 4.0 - installed | install not required", + testDir: filepath.Join("projects", "package-managers", "nuget", "single4.0"), + successfulTreeBuiltExpected: true, + }, + { + name: "nuget single 5.0 - not installed | install required - install command", + testDir: filepath.Join("projects", "package-managers", "nuget", "single5.0"), + installCommand: "nuget restore", // todo test in ci with nuget restore + successfulTreeBuiltExpected: true, + }, + { + name: "nuget single 5.0 - not installed | install required - install forbidden", + testDir: filepath.Join("projects", "package-managers", "nuget", "single5.0"), + successfulTreeBuiltExpected: false, + }, + { + name: "nuget multi - not installed | install required - install command", + testDir: filepath.Join("projects", "package-managers", "nuget", "multi"), + installCommand: "nuget restore", // todo test in ci with nuget restore + successfulTreeBuiltExpected: true, + }, + { + name: "nuget multi - not installed | install required - install forbidden", + testDir: filepath.Join("projects", "package-managers", "nuget", "multi"), + successfulTreeBuiltExpected: false, + }, + { + name: "dotnet-single - not installed | install required - install forbidden", + testDir: filepath.Join("projects", "package-managers", "dotnet", "dotnet-single"), + successfulTreeBuiltExpected: false, + }, + { + name: "dotnet-multi - not installed | install required - install forbidden", + testDir: filepath.Join("projects", "package-managers", "dotnet", "dotnet-multi"), + successfulTreeBuiltExpected: false, + }, + } + + for _, test := range testCases { + t.Run(test.name, func(t *testing.T) { + // Create and change directory to test workspace + _, cleanUp := sca.CreateTestWorkspace(t, test.testDir) + defer cleanUp() + + params := (&xrayUtils2.AuditBasicParams{}).SetSkipAutoInstall(true) + if test.installCommand != "" { + splitInstallCommand := strings.Split(test.installCommand, " ") + params = params.SetInstallCommandName(splitInstallCommand[0]).SetInstallCommandArgs(splitInstallCommand[1:]) + } + + dependencyTrees, uniqueDeps, err := BuildDependencyTree(params) + if !test.successfulTreeBuiltExpected { + assert.Nil(t, dependencyTrees) + assert.Nil(t, uniqueDeps) + assert.Error(t, err) + assert.IsType(t, &utils.ErrProjectNotInstalled{}, err) + } else { + assert.NotNil(t, dependencyTrees) + assert.NotNil(t, uniqueDeps) + assert.NoError(t, err) + } + }) + } +}