github.com/jfrog/frogbot@v1.1.1-0.20231221090046-821a26f50338/scanrepository/scanrepository_test.go (about)

     1  package scanrepository
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"github.com/google/go-github/v45/github"
     7  	biutils "github.com/jfrog/build-info-go/utils"
     8  	"github.com/jfrog/frogbot/utils"
     9  	"github.com/jfrog/frogbot/utils/outputwriter"
    10  	"github.com/jfrog/froggit-go/vcsclient"
    11  	"github.com/jfrog/froggit-go/vcsutils"
    12  	"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
    13  	"github.com/jfrog/jfrog-cli-core/v2/xray/formats"
    14  	xrayutils "github.com/jfrog/jfrog-cli-core/v2/xray/utils"
    15  	"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
    16  	"github.com/jfrog/jfrog-client-go/utils/log"
    17  	"github.com/jfrog/jfrog-client-go/xray/services"
    18  	"github.com/stretchr/testify/assert"
    19  	"github.com/stretchr/testify/require"
    20  	"net/http/httptest"
    21  	"os"
    22  	"os/exec"
    23  	"path/filepath"
    24  	"strings"
    25  	"testing"
    26  )
    27  
    28  const rootTestDir = "scanrepository"
    29  
    30  var testPackagesData = []struct {
    31  	packageType string
    32  	commandName string
    33  	commandArgs []string
    34  }{
    35  	{
    36  		packageType: coreutils.Go.String(),
    37  	},
    38  	{
    39  		packageType: coreutils.Maven.String(),
    40  	},
    41  	{
    42  		packageType: coreutils.Gradle.String(),
    43  	},
    44  	{
    45  		packageType: coreutils.Npm.String(),
    46  		commandName: "npm",
    47  		commandArgs: []string{"install"},
    48  	},
    49  	{
    50  		packageType: "yarn1",
    51  		commandName: "yarn",
    52  		commandArgs: []string{"install"},
    53  	},
    54  	{
    55  		packageType: "yarn2",
    56  		commandName: "yarn",
    57  		commandArgs: []string{"install"},
    58  	},
    59  	{
    60  		packageType: coreutils.Dotnet.String(),
    61  		commandName: "dotnet",
    62  		commandArgs: []string{"restore"},
    63  	},
    64  	{
    65  		packageType: coreutils.Nuget.String(),
    66  		commandName: "nuget",
    67  		commandArgs: []string{"restore"},
    68  	},
    69  	{
    70  		packageType: coreutils.Pip.String(),
    71  	},
    72  	{
    73  		packageType: coreutils.Pipenv.String(),
    74  	},
    75  	{
    76  		packageType: coreutils.Poetry.String(),
    77  	},
    78  }
    79  
    80  func TestScanRepositoryCmd_Run(t *testing.T) {
    81  	tests := []struct {
    82  		testName                       string
    83  		configPath                     string
    84  		expectedPackagesInBranch       map[string][]string
    85  		expectedVersionUpdatesInBranch map[string][]string
    86  		packageDescriptorPaths         []string
    87  		aggregateFixes                 bool
    88  	}{
    89  		{
    90  			testName:                       "aggregate",
    91  			expectedPackagesInBranch:       map[string][]string{"frogbot-update-npm-dependencies-master": {"uuid", "minimist", "mpath"}},
    92  			expectedVersionUpdatesInBranch: map[string][]string{"frogbot-update-npm-dependencies-master": {"^1.2.6", "^9.0.0", "^0.8.4"}},
    93  			packageDescriptorPaths:         []string{"package.json"},
    94  			aggregateFixes:                 true,
    95  		},
    96  		{
    97  			testName:                       "aggregate-multi-dir",
    98  			expectedPackagesInBranch:       map[string][]string{"frogbot-update-npm-dependencies-master": {"uuid", "minimatch", "mpath", "minimist"}},
    99  			expectedVersionUpdatesInBranch: map[string][]string{"frogbot-update-npm-dependencies-master": {"^1.2.6", "^9.0.0", "^0.8.4", "^3.0.5"}},
   100  			packageDescriptorPaths:         []string{"npm1/package.json", "npm2/package.json"},
   101  			aggregateFixes:                 true,
   102  			configPath:                     "../testdata/scanrepository/cmd/aggregate-multi-dir/.frogbot/frogbot-config.yml",
   103  		},
   104  		{
   105  			testName:                       "aggregate-multi-project",
   106  			expectedPackagesInBranch:       map[string][]string{"frogbot-update-npm-dependencies-master": {"uuid", "minimatch", "mpath"}, "frogbot-update-Pip-dependencies-master": {"pyjwt", "pexpect"}},
   107  			expectedVersionUpdatesInBranch: map[string][]string{"frogbot-update-npm-dependencies-master": {"^9.0.0", "^0.8.4", "^3.0.5"}, "frogbot-update-Pip-dependencies-master": {"2.4.0"}},
   108  			packageDescriptorPaths:         []string{"npm/package.json", "pip/requirements.txt"},
   109  			aggregateFixes:                 true,
   110  			configPath:                     "../testdata/scanrepository/cmd/aggregate-multi-project/.frogbot/frogbot-config.yml",
   111  		},
   112  		{
   113  			testName: "aggregate-no-vul",
   114  			// No branch is being created because there are no vulnerabilities.
   115  			expectedPackagesInBranch:       map[string][]string{"master": {}},
   116  			expectedVersionUpdatesInBranch: map[string][]string{"master": {}},
   117  			packageDescriptorPaths:         []string{"package.json"},
   118  			aggregateFixes:                 true,
   119  		},
   120  		{
   121  			testName: "aggregate-cant-fix",
   122  			// Branch name stays master as no new branch is being created
   123  			expectedPackagesInBranch:       map[string][]string{"master": {}},
   124  			expectedVersionUpdatesInBranch: map[string][]string{"master": {}},
   125  			// This is a build tool dependency which should not be fixed.
   126  			packageDescriptorPaths: []string{"setup.py"},
   127  			aggregateFixes:         true,
   128  		},
   129  		{
   130  			testName:                       "non-aggregate",
   131  			expectedPackagesInBranch:       map[string][]string{"frogbot-minimist-258ad6a538b5ba800f18ae4f6d660302": {"minimist"}},
   132  			expectedVersionUpdatesInBranch: map[string][]string{"frogbot-minimist-258ad6a538b5ba800f18ae4f6d660302": {"^1.2.6"}},
   133  			packageDescriptorPaths:         []string{"package.json"},
   134  			aggregateFixes:                 false,
   135  		},
   136  	}
   137  	baseDir, err := os.Getwd()
   138  	assert.NoError(t, err)
   139  	testDir, cleanup := utils.CopyTestdataProjectsToTemp(t, filepath.Join(rootTestDir, "cmd"))
   140  	defer cleanup()
   141  	for _, test := range tests {
   142  		t.Run(test.testName, func(t *testing.T) {
   143  			// Prepare
   144  			serverParams, restoreEnv := utils.VerifyEnv(t)
   145  			defer restoreEnv()
   146  			if test.aggregateFixes {
   147  				assert.NoError(t, os.Setenv(utils.GitAggregateFixesEnv, "true"))
   148  				defer func() {
   149  					assert.NoError(t, os.Setenv(utils.GitAggregateFixesEnv, "false"))
   150  				}()
   151  			}
   152  			var port string
   153  			server := httptest.NewServer(createScanRepoGitHubHandler(t, &port, nil, test.testName))
   154  			defer server.Close()
   155  			port = server.URL[strings.LastIndex(server.URL, ":")+1:]
   156  			gitTestParams := utils.Git{
   157  				GitProvider: vcsutils.GitHub,
   158  				VcsInfo: vcsclient.VcsInfo{
   159  					Token:       "123456",
   160  					APIEndpoint: server.URL,
   161  				},
   162  				RepoName:  test.testName,
   163  				RepoOwner: "jfrog",
   164  			}
   165  			client, err := vcsclient.NewClientBuilder(vcsutils.GitHub).ApiEndpoint(server.URL).Token("123456").Build()
   166  			assert.NoError(t, err)
   167  
   168  			// Read config or resolve to default
   169  			var configData []byte
   170  			if test.configPath != "" {
   171  				configData, err = utils.ReadConfigFromFileSystem(test.configPath)
   172  				assert.NoError(t, err)
   173  			} else {
   174  				configData = []byte{}
   175  				// Manual set of "JF_GIT_BASE_BRANCH"
   176  				gitTestParams.Branches = []string{"master"}
   177  			}
   178  
   179  			utils.CreateDotGitWithCommit(t, testDir, port, test.testName)
   180  			configAggregator, err := utils.BuildRepoAggregator(configData, &gitTestParams, &serverParams, utils.ScanRepository)
   181  			assert.NoError(t, err)
   182  			// Run
   183  			var cmd = ScanRepositoryCmd{dryRun: true, dryRunRepoPath: testDir}
   184  			err = cmd.Run(configAggregator, client, utils.MockHasConnection())
   185  			defer func() {
   186  				assert.NoError(t, os.Chdir(baseDir))
   187  			}()
   188  
   189  			// Validate
   190  			assert.NoError(t, err)
   191  			for branch, packages := range test.expectedPackagesInBranch {
   192  				resultDiff, err := verifyDependencyFileDiff("master", branch, test.packageDescriptorPaths...)
   193  				assert.NoError(t, err)
   194  				if len(packages) > 0 {
   195  					assert.NotEmpty(t, resultDiff)
   196  				}
   197  				for _, packageToUpdate := range packages {
   198  					assert.Contains(t, string(resultDiff), packageToUpdate)
   199  				}
   200  				packageVersionUpdatesInBranch := test.expectedVersionUpdatesInBranch[branch]
   201  				for _, updatedVersion := range packageVersionUpdatesInBranch {
   202  					assert.Contains(t, string(resultDiff), updatedVersion)
   203  				}
   204  			}
   205  		})
   206  	}
   207  }
   208  
   209  // Tests the lifecycle of aggregated pull request
   210  // No open pull request -> Open
   211  // If Pull request already active, compare scan results for current and remote branch
   212  // Same scan results -> do nothing.
   213  // Different scan results -> Update the pull request branch & body.
   214  func TestAggregatePullRequestLifecycle(t *testing.T) {
   215  	mockPrId := 1
   216  	sourceBranchName := "frogbot-update-npm-dependencies"
   217  	targetBranchName := "main"
   218  	sourceLabel := "repo:frogbot-update-npm-dependencies"
   219  	targetLabel := "repo:main"
   220  	firstBody := `
   221  [comment]: <> (Checksum: 4608a55b621cb6337ac93487979ac09c)
   222  pr body
   223  `
   224  	secondBody := `
   225  [comment]: <> (Checksum: 01373ac4d2c32e7da9be22f3e4b4e665)
   226  pr body
   227   `
   228  	tests := []struct {
   229  		testName                string
   230  		expectedUpdate          bool
   231  		mockPullRequestResponse []*github.PullRequest
   232  	}{
   233  		{
   234  			testName:       "aggregate-dont-update-pr",
   235  			expectedUpdate: false,
   236  			mockPullRequestResponse: []*github.PullRequest{{
   237  				Number: &mockPrId,
   238  				Head: &github.PullRequestBranch{
   239  					Label: &sourceLabel,
   240  					Repo:  &github.Repository{Name: &sourceBranchName, Owner: &github.User{}},
   241  				},
   242  				Base: &github.PullRequestBranch{
   243  					Label: &targetLabel,
   244  					Repo:  &github.Repository{Name: &targetBranchName, Owner: &github.User{}},
   245  				},
   246  				Body: &firstBody,
   247  			}},
   248  		},
   249  		{
   250  			testName:       "aggregate-update-pr",
   251  			expectedUpdate: true,
   252  			mockPullRequestResponse: []*github.PullRequest{{
   253  				Number: &mockPrId,
   254  				Head: &github.PullRequestBranch{
   255  					Label: &sourceLabel,
   256  					Repo:  &github.Repository{Name: &sourceBranchName, Owner: &github.User{}},
   257  				},
   258  				Base: &github.PullRequestBranch{
   259  					Label: &targetLabel,
   260  					Repo:  &github.Repository{Name: &targetBranchName, Owner: &github.User{}},
   261  				},
   262  				Body: &secondBody,
   263  			}},
   264  		},
   265  	}
   266  
   267  	baseDir, err := os.Getwd()
   268  	assert.NoError(t, err)
   269  	serverParams, restoreEnv := utils.VerifyEnv(t)
   270  	defer restoreEnv()
   271  	testDir, cleanup := utils.CopyTestdataProjectsToTemp(t, filepath.Join(rootTestDir, "aggregate-pr-lifecycle"))
   272  	defer cleanup()
   273  	for _, test := range tests {
   274  		t.Run(test.testName, func(t *testing.T) {
   275  			var port string
   276  			server := httptest.NewServer(createScanRepoGitHubHandler(t, &port, test.mockPullRequestResponse, test.testName))
   277  			defer server.Close()
   278  			port = server.URL[strings.LastIndex(server.URL, ":")+1:]
   279  
   280  			assert.NoError(t, os.Setenv(utils.GitAggregateFixesEnv, "true"))
   281  			defer func() {
   282  				assert.NoError(t, os.Setenv(utils.GitAggregateFixesEnv, "false"))
   283  			}()
   284  
   285  			gitTestParams := &utils.Git{
   286  				GitProvider: vcsutils.GitHub,
   287  				RepoOwner:   "jfrog",
   288  				VcsInfo: vcsclient.VcsInfo{
   289  					Token:       "123456",
   290  					APIEndpoint: server.URL,
   291  				}, RepoName: test.testName,
   292  			}
   293  
   294  			utils.CreateDotGitWithCommit(t, testDir, port, test.testName)
   295  			client, err := vcsclient.NewClientBuilder(vcsutils.GitHub).ApiEndpoint(server.URL).Token("123456").Build()
   296  			assert.NoError(t, err)
   297  			// Load default configurations
   298  			var configData []byte
   299  			gitTestParams.Branches = []string{"master"}
   300  			configAggregator, err := utils.BuildRepoAggregator(configData, gitTestParams, &serverParams, utils.ScanRepository)
   301  			assert.NoError(t, err)
   302  			// Run
   303  			var cmd = ScanRepositoryCmd{dryRun: true, dryRunRepoPath: testDir}
   304  			err = cmd.Run(configAggregator, client, utils.MockHasConnection())
   305  			defer func() {
   306  				assert.NoError(t, os.Chdir(baseDir))
   307  			}()
   308  			assert.NoError(t, err)
   309  		})
   310  	}
   311  }
   312  
   313  // /      1.0         --> 1.0 ≤ x
   314  // /      (,1.0]      --> x ≤ 1.0
   315  // /      (,1.0)      --> x < 1.0
   316  // /      [1.0]       --> x == 1.0
   317  // /      (1.0,)      --> 1.0 < x
   318  // /      (1.0, 2.0)   --> 1.0 < x < 2.0
   319  // /      [1.0, 2.0]   --> 1.0 ≤ x ≤ 2.0
   320  func TestParseVersionChangeString(t *testing.T) {
   321  	tests := []struct {
   322  		versionChangeString string
   323  		expectedVersion     string
   324  	}{
   325  		{"1.2.3", "1.2.3"},
   326  		{"[1.2.3]", "1.2.3"},
   327  		{"[1.2.3, 2.0.0]", "1.2.3"},
   328  
   329  		{"(,1.2.3]", ""},
   330  		{"(,1.2.3)", ""},
   331  		{"(1.2.3,)", ""},
   332  		{"(1.2.3, 2.0.0)", ""},
   333  	}
   334  
   335  	for _, test := range tests {
   336  		t.Run(test.versionChangeString, func(t *testing.T) {
   337  			assert.Equal(t, test.expectedVersion, parseVersionChangeString(test.versionChangeString))
   338  		})
   339  	}
   340  }
   341  
   342  func TestGenerateFixBranchName(t *testing.T) {
   343  	tests := []struct {
   344  		baseBranch      string
   345  		impactedPackage string
   346  		fixVersion      string
   347  		expectedName    string
   348  	}{
   349  		{"dev", "gopkg.in/yaml.v3", "3.0.0", "frogbot-gopkg.in/yaml.v3-d61bde82dc594e5ccc5a042fe224bf7c"},
   350  		{"master", "gopkg.in/yaml.v3", "3.0.0", "frogbot-gopkg.in/yaml.v3-41405528994061bd108e3bbd4c039a03"},
   351  		{"dev", "replace:colons:colons", "3.0.0", "frogbot-replace_colons_colons-89e555131b4a70a32fe9d9c44d6ff0fc"},
   352  	}
   353  	gitManager := utils.GitManager{}
   354  	for _, test := range tests {
   355  		t.Run(test.expectedName, func(t *testing.T) {
   356  			branchName, err := gitManager.GenerateFixBranchName(test.baseBranch, test.impactedPackage, test.fixVersion)
   357  			assert.NoError(t, err)
   358  			assert.Equal(t, test.expectedName, branchName)
   359  		})
   360  	}
   361  }
   362  
   363  func TestPackageTypeFromScan(t *testing.T) {
   364  	environmentVars, restoreEnv := utils.VerifyEnv(t)
   365  	defer restoreEnv()
   366  	testScan := &ScanRepositoryCmd{OutputWriter: &outputwriter.StandardOutput{}}
   367  	trueVal := true
   368  	params := utils.Params{
   369  		Scan: utils.Scan{Projects: []utils.Project{{UseWrapper: &trueVal}}},
   370  	}
   371  	var frogbotParams = utils.Repository{
   372  		Server: environmentVars,
   373  		Params: params,
   374  	}
   375  	for _, pkg := range testPackagesData {
   376  		// Create temp technology project
   377  		projectPath := filepath.Join("..", "testdata", "projects", pkg.packageType)
   378  		t.Run(pkg.packageType, func(t *testing.T) {
   379  			tmpDir, err := fileutils.CreateTempDir()
   380  			defer func() {
   381  				err = fileutils.RemoveTempDir(tmpDir)
   382  			}()
   383  			assert.NoError(t, err)
   384  			assert.NoError(t, biutils.CopyDir(projectPath, tmpDir, true, nil))
   385  			if pkg.packageType == coreutils.Gradle.String() {
   386  				assert.NoError(t, os.Chmod(filepath.Join(tmpDir, "gradlew"), 0777))
   387  				assert.NoError(t, os.Chmod(filepath.Join(tmpDir, "gradlew.bat"), 0777))
   388  			}
   389  			frogbotParams.Projects[0].WorkingDirs = []string{tmpDir}
   390  			files, err := fileutils.ListFiles(tmpDir, true)
   391  			assert.NoError(t, err)
   392  			for _, file := range files {
   393  				log.Info(file)
   394  			}
   395  			frogbotParams.Projects[0].InstallCommandName = pkg.commandName
   396  			frogbotParams.Projects[0].InstallCommandArgs = pkg.commandArgs
   397  			scanSetup := utils.ScanDetails{
   398  				XrayGraphScanParams: &services.XrayGraphScanParams{},
   399  				Project:             &frogbotParams.Projects[0],
   400  				ServerDetails:       &frogbotParams.Server,
   401  			}
   402  			testScan.scanDetails = &scanSetup
   403  			scanResponse, err := testScan.scan(tmpDir)
   404  			assert.NoError(t, err)
   405  			verifyTechnologyNaming(t, scanResponse.GetScaScansXrayResults(), pkg.packageType)
   406  		})
   407  	}
   408  }
   409  
   410  func TestGetMinimalFixVersion(t *testing.T) {
   411  	tests := []struct {
   412  		impactedVersionPackage string
   413  		fixVersions            []string
   414  		expected               string
   415  	}{
   416  		{impactedVersionPackage: "1.6.2", fixVersions: []string{"1.5.3", "1.6.1", "1.6.22", "1.7.0"}, expected: "1.6.22"},
   417  		{impactedVersionPackage: "v1.6.2", fixVersions: []string{"1.5.3", "1.6.1", "1.6.22", "1.7.0"}, expected: "1.6.22"},
   418  		{impactedVersionPackage: "1.7.1", fixVersions: []string{"1.5.3", "1.6.1", "1.6.22", "1.7.0"}, expected: ""},
   419  		{impactedVersionPackage: "1.7.1", fixVersions: []string{"2.5.3"}, expected: "2.5.3"},
   420  		{impactedVersionPackage: "v1.7.1", fixVersions: []string{"0.5.3", "0.9.9"}, expected: ""},
   421  	}
   422  	for _, test := range tests {
   423  		t.Run(test.expected, func(t *testing.T) {
   424  			expected := getMinimalFixVersion(test.impactedVersionPackage, test.fixVersions)
   425  			assert.Equal(t, test.expected, expected)
   426  		})
   427  	}
   428  }
   429  
   430  func TestCreateVulnerabilitiesMap(t *testing.T) {
   431  	cfp := &ScanRepositoryCmd{}
   432  
   433  	testCases := []struct {
   434  		name            string
   435  		scanResults     *xrayutils.Results
   436  		isMultipleRoots bool
   437  		expectedMap     map[string]*utils.VulnerabilityDetails
   438  	}{
   439  		{
   440  			name: "Scan results with no violations and vulnerabilities",
   441  			scanResults: &xrayutils.Results{
   442  				ScaResults:          []xrayutils.ScaScanResult{},
   443  				ExtendedScanResults: &xrayutils.ExtendedScanResults{},
   444  			},
   445  			expectedMap: map[string]*utils.VulnerabilityDetails{},
   446  		},
   447  		{
   448  			name: "Scan results with vulnerabilities and no violations",
   449  			scanResults: &xrayutils.Results{
   450  				ScaResults: []xrayutils.ScaScanResult{{
   451  					XrayResults: []services.ScanResponse{
   452  						{
   453  							Vulnerabilities: []services.Vulnerability{
   454  								{
   455  									Cves: []services.Cve{
   456  										{Id: "CVE-2023-1234", CvssV3Score: "9.1"},
   457  										{Id: "CVE-2023-4321", CvssV3Score: "8.9"},
   458  									},
   459  									Severity: "Critical",
   460  									Components: map[string]services.Component{
   461  										"vuln1": {
   462  											FixedVersions: []string{"1.9.1", "2.0.3", "2.0.5"},
   463  											ImpactPaths:   [][]services.ImpactPathNode{{{ComponentId: "root"}, {ComponentId: "vuln1"}}},
   464  										},
   465  									},
   466  								},
   467  								{
   468  									Cves: []services.Cve{
   469  										{Id: "CVE-2022-1234", CvssV3Score: "7.1"},
   470  										{Id: "CVE-2022-4321", CvssV3Score: "7.9"},
   471  									},
   472  									Severity: "High",
   473  									Components: map[string]services.Component{
   474  										"vuln2": {
   475  											FixedVersions: []string{"2.4.1", "2.6.3", "2.8.5"},
   476  											ImpactPaths:   [][]services.ImpactPathNode{{{ComponentId: "root"}, {ComponentId: "vuln1"}, {ComponentId: "vuln2"}}},
   477  										},
   478  									},
   479  								},
   480  							},
   481  						},
   482  					},
   483  				}},
   484  				ExtendedScanResults: &xrayutils.ExtendedScanResults{},
   485  			},
   486  			expectedMap: map[string]*utils.VulnerabilityDetails{
   487  				"vuln1": {
   488  					SuggestedFixedVersion: "1.9.1",
   489  					IsDirectDependency:    true,
   490  					Cves:                  []string{"CVE-2023-1234", "CVE-2023-4321"},
   491  				},
   492  				"vuln2": {
   493  					SuggestedFixedVersion: "2.4.1",
   494  					Cves:                  []string{"CVE-2022-1234", "CVE-2022-4321"},
   495  				},
   496  			},
   497  		},
   498  		{
   499  			name: "Scan results with violations and no vulnerabilities",
   500  			scanResults: &xrayutils.Results{
   501  				ScaResults: []xrayutils.ScaScanResult{{
   502  					XrayResults: []services.ScanResponse{
   503  						{
   504  							Violations: []services.Violation{
   505  								{
   506  									ViolationType: "security",
   507  									Cves: []services.Cve{
   508  										{Id: "CVE-2023-1234", CvssV3Score: "9.1"},
   509  										{Id: "CVE-2023-4321", CvssV3Score: "8.9"},
   510  									},
   511  									Severity: "Critical",
   512  									Components: map[string]services.Component{
   513  										"viol1": {
   514  											FixedVersions: []string{"1.9.1", "2.0.3", "2.0.5"},
   515  											ImpactPaths:   [][]services.ImpactPathNode{{{ComponentId: "root"}, {ComponentId: "viol1"}}},
   516  										},
   517  									},
   518  								},
   519  								{
   520  									ViolationType: "security",
   521  									Cves: []services.Cve{
   522  										{Id: "CVE-2022-1234", CvssV3Score: "7.1"},
   523  										{Id: "CVE-2022-4321", CvssV3Score: "7.9"},
   524  									},
   525  									Severity: "High",
   526  									Components: map[string]services.Component{
   527  										"viol2": {
   528  											FixedVersions: []string{"2.4.1", "2.6.3", "2.8.5"},
   529  											ImpactPaths:   [][]services.ImpactPathNode{{{ComponentId: "root"}, {ComponentId: "viol1"}, {ComponentId: "viol2"}}},
   530  										},
   531  									},
   532  								},
   533  							},
   534  						},
   535  					},
   536  				}},
   537  				ExtendedScanResults: &xrayutils.ExtendedScanResults{},
   538  			},
   539  			expectedMap: map[string]*utils.VulnerabilityDetails{
   540  				"viol1": {
   541  					SuggestedFixedVersion: "1.9.1",
   542  					IsDirectDependency:    true,
   543  					Cves:                  []string{"CVE-2023-1234", "CVE-2023-4321"},
   544  				},
   545  				"viol2": {
   546  					SuggestedFixedVersion: "2.4.1",
   547  					Cves:                  []string{"CVE-2022-1234", "CVE-2022-4321"},
   548  				},
   549  			},
   550  		},
   551  	}
   552  
   553  	for _, testCase := range testCases {
   554  		t.Run(testCase.name, func(t *testing.T) {
   555  			fixVersionsMap, err := cfp.createVulnerabilitiesMap(testCase.scanResults, testCase.isMultipleRoots)
   556  			assert.NoError(t, err)
   557  			for name, expectedVuln := range testCase.expectedMap {
   558  				actualVuln, exists := fixVersionsMap[name]
   559  				require.True(t, exists)
   560  				assert.Equal(t, expectedVuln.IsDirectDependency, actualVuln.IsDirectDependency)
   561  				assert.Equal(t, expectedVuln.SuggestedFixedVersion, actualVuln.SuggestedFixedVersion)
   562  				assert.ElementsMatch(t, expectedVuln.Cves, actualVuln.Cves)
   563  			}
   564  		})
   565  	}
   566  }
   567  
   568  // Verifies unsupported packages return specific error
   569  // Other logic is implemented inside each package-handler.
   570  func TestUpdatePackageToFixedVersion(t *testing.T) {
   571  	var testScan ScanRepositoryCmd
   572  	for tech, buildToolsDependencies := range utils.BuildToolsDependenciesMap {
   573  		for _, impactedDependency := range buildToolsDependencies {
   574  			vulnDetails := &utils.VulnerabilityDetails{SuggestedFixedVersion: "3.3.3", VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{Technology: tech, ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ImpactedDependencyName: impactedDependency}}, IsDirectDependency: true}
   575  			err := testScan.updatePackageToFixedVersion(vulnDetails)
   576  			assert.Error(t, err, "Expected error to occur")
   577  			assert.IsType(t, &utils.ErrUnsupportedFix{}, err, "Expected unsupported fix error")
   578  		}
   579  	}
   580  }
   581  
   582  func TestGetRemoteBranchScanHash(t *testing.T) {
   583  	prBody := `
   584  a body
   585  
   586  [Comment]: <> (Checksum: myhash4321)
   587  `
   588  	cfp := &ScanRepositoryCmd{}
   589  	result := cfp.getRemoteBranchScanHash(prBody)
   590  	assert.Equal(t, "myhash4321", result)
   591  	prBody = `
   592  random body
   593  `
   594  	result = cfp.getRemoteBranchScanHash(prBody)
   595  	assert.Equal(t, "", result)
   596  }
   597  
   598  func TestPreparePullRequestDetails(t *testing.T) {
   599  	cfp := ScanRepositoryCmd{OutputWriter: &outputwriter.StandardOutput{}, gitManager: &utils.GitManager{}}
   600  	cfp.OutputWriter.SetJasOutputFlags(true, false)
   601  	vulnerabilities := []*utils.VulnerabilityDetails{
   602  		{
   603  			VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{
   604  				Summary: "summary",
   605  				ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
   606  					SeverityDetails:           formats.SeverityDetails{Severity: "High", SeverityNumValue: 10},
   607  					ImpactedDependencyName:    "package1",
   608  					ImpactedDependencyVersion: "1.0.0",
   609  				},
   610  				FixedVersions: []string{"1.0.0", "2.0.0"},
   611  				Cves:          []formats.CveRow{{Id: "CVE-2022-1234"}},
   612  			},
   613  			SuggestedFixedVersion: "1.0.0",
   614  		},
   615  	}
   616  	expectedPrBody := utils.GenerateFixPullRequestDetails(utils.ExtractVulnerabilitiesDetailsToRows(vulnerabilities), cfp.OutputWriter)
   617  	prTitle, prBody, err := cfp.preparePullRequestDetails(vulnerabilities...)
   618  	assert.NoError(t, err)
   619  	assert.Equal(t, "[🐸 Frogbot] Update version of package1 to 1.0.0", prTitle)
   620  	assert.Equal(t, expectedPrBody, prBody)
   621  	vulnerabilities = append(vulnerabilities, &utils.VulnerabilityDetails{
   622  		VulnerabilityOrViolationRow: formats.VulnerabilityOrViolationRow{
   623  			Summary: "summary",
   624  			ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
   625  				SeverityDetails:           formats.SeverityDetails{Severity: "Critical", SeverityNumValue: 12},
   626  				ImpactedDependencyName:    "package2",
   627  				ImpactedDependencyVersion: "2.0.0",
   628  			},
   629  			FixedVersions: []string{"2.0.0", "3.0.0"},
   630  			Cves:          []formats.CveRow{{Id: "CVE-2022-4321"}},
   631  		},
   632  		SuggestedFixedVersion: "2.0.0",
   633  	})
   634  	cfp.aggregateFixes = true
   635  	expectedPrBody = utils.GenerateFixPullRequestDetails(utils.ExtractVulnerabilitiesDetailsToRows(vulnerabilities), cfp.OutputWriter) + outputwriter.MarkdownComment("Checksum: bec823edaceb5d0478b789798e819bde")
   636  	prTitle, prBody, err = cfp.preparePullRequestDetails(vulnerabilities...)
   637  	assert.NoError(t, err)
   638  	assert.Equal(t, cfp.gitManager.GenerateAggregatedPullRequestTitle([]coreutils.Technology{}), prTitle)
   639  	assert.Equal(t, expectedPrBody, prBody)
   640  	cfp.OutputWriter = &outputwriter.SimplifiedOutput{}
   641  	expectedPrBody = utils.GenerateFixPullRequestDetails(utils.ExtractVulnerabilitiesDetailsToRows(vulnerabilities), cfp.OutputWriter) + outputwriter.MarkdownComment("Checksum: bec823edaceb5d0478b789798e819bde")
   642  	prTitle, prBody, err = cfp.preparePullRequestDetails(vulnerabilities...)
   643  	assert.NoError(t, err)
   644  	assert.Equal(t, cfp.gitManager.GenerateAggregatedPullRequestTitle([]coreutils.Technology{}), prTitle)
   645  	assert.Equal(t, expectedPrBody, prBody)
   646  }
   647  
   648  func verifyTechnologyNaming(t *testing.T, scanResponse []services.ScanResponse, expectedType string) {
   649  	for _, resp := range scanResponse {
   650  		for _, vulnerability := range resp.Vulnerabilities {
   651  			assert.Equal(t, expectedType, vulnerability.Technology)
   652  		}
   653  	}
   654  }
   655  
   656  // Executing git diff to ensure that the intended changes to the dependent file have been made
   657  func verifyDependencyFileDiff(baseBranch string, fixBranch string, packageDescriptorPaths ...string) (output []byte, err error) {
   658  	log.Debug(fmt.Sprintf("Checking differences in %s between branches %s and %s", packageDescriptorPaths, baseBranch, fixBranch))
   659  	// Suppress condition always false warning
   660  	//goland:noinspection ALL
   661  	var args []string
   662  	if coreutils.IsWindows() {
   663  		args = []string{"/c", "git", "diff", baseBranch, fixBranch}
   664  		args = append(args, packageDescriptorPaths...)
   665  		output, err = exec.Command("cmd", args...).Output()
   666  	} else {
   667  		args = []string{"diff", baseBranch, fixBranch}
   668  		args = append(args, packageDescriptorPaths...)
   669  		output, err = exec.Command("git", args...).Output()
   670  	}
   671  	var exitError *exec.ExitError
   672  	if errors.As(err, &exitError) {
   673  		err = errors.New("git error: " + string(exitError.Stderr))
   674  	}
   675  	return
   676  }