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

     1  package scanpullrequest
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"net/http/httptest"
    11  	"os"
    12  	"path/filepath"
    13  	"strings"
    14  	"testing"
    15  	"time"
    16  
    17  	"github.com/jfrog/frogbot/utils"
    18  	"github.com/jfrog/frogbot/utils/outputwriter"
    19  	"github.com/jfrog/froggit-go/vcsclient"
    20  	"github.com/jfrog/froggit-go/vcsutils"
    21  	coreconfig "github.com/jfrog/jfrog-cli-core/v2/utils/config"
    22  	"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
    23  	"github.com/jfrog/jfrog-cli-core/v2/xray/formats"
    24  	xrayutils "github.com/jfrog/jfrog-cli-core/v2/xray/utils"
    25  	"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
    26  	"github.com/jfrog/jfrog-client-go/utils/log"
    27  	"github.com/jfrog/jfrog-client-go/xray/services"
    28  	"github.com/owenrumney/go-sarif/v2/sarif"
    29  	"github.com/stretchr/testify/assert"
    30  )
    31  
    32  const (
    33  	testMultiDirProjConfigPath       = "testdata/config/frogbot-config-multi-dir-test-proj.yml"
    34  	testMultiDirProjConfigPathNoFail = "testdata/config/frogbot-config-multi-dir-test-proj-no-fail.yml"
    35  	testProjSubdirConfigPath         = "testdata/config/frogbot-config-test-proj-subdir.yml"
    36  	testCleanProjConfigPath          = "testdata/config/frogbot-config-clean-test-proj.yml"
    37  	testProjConfigPath               = "testdata/config/frogbot-config-test-proj.yml"
    38  	testProjConfigPathNoFail         = "testdata/config/frogbot-config-test-proj-no-fail.yml"
    39  	testSourceBranchName             = "pr"
    40  	testTargetBranchName             = "master"
    41  )
    42  
    43  func TestCreateVulnerabilitiesRows(t *testing.T) {
    44  	// Previous scan with only one violation - XRAY-1
    45  	previousScan := services.ScanResponse{
    46  		Violations: []services.Violation{
    47  			{
    48  				IssueId:       "XRAY-1",
    49  				Summary:       "summary-1",
    50  				Severity:      "high",
    51  				Cves:          []services.Cve{},
    52  				ViolationType: "security",
    53  				Components:    map[string]services.Component{"component-A": {}, "component-B": {}},
    54  			},
    55  			{
    56  				IssueId:       "XRAY-4",
    57  				ViolationType: "license",
    58  				LicenseKey:    "Apache-2.0",
    59  				Components:    map[string]services.Component{"Dep-2": {}},
    60  			},
    61  		},
    62  	}
    63  
    64  	// Current scan with 2 violations - XRAY-1 and XRAY-2
    65  	currentScan := services.ScanResponse{
    66  		Violations: []services.Violation{
    67  			{
    68  				IssueId:       "XRAY-1",
    69  				Summary:       "summary-1",
    70  				Severity:      "high",
    71  				ViolationType: "security",
    72  				Components:    map[string]services.Component{"component-A": {}, "component-B": {}},
    73  			},
    74  			{
    75  				IssueId:       "XRAY-2",
    76  				Summary:       "summary-2",
    77  				ViolationType: "security",
    78  				Severity:      "low",
    79  				Components:    map[string]services.Component{"component-C": {}, "component-D": {}},
    80  			},
    81  			{
    82  				IssueId:       "XRAY-3",
    83  				ViolationType: "license",
    84  				LicenseKey:    "MIT",
    85  				Components:    map[string]services.Component{"Dep-1": {}},
    86  			},
    87  		},
    88  	}
    89  
    90  	// Run createNewIssuesRows and make sure that only the XRAY-2 violation exists in the results
    91  	securityViolationsRows, licenseViolations, err := createNewVulnerabilitiesRows(
    92  		&xrayutils.Results{ScaResults: []xrayutils.ScaScanResult{{XrayResults: []services.ScanResponse{previousScan}}}, ExtendedScanResults: &xrayutils.ExtendedScanResults{}},
    93  		&xrayutils.Results{ScaResults: []xrayutils.ScaScanResult{{XrayResults: []services.ScanResponse{currentScan}}}, ExtendedScanResults: &xrayutils.ExtendedScanResults{}},
    94  		nil,
    95  	)
    96  	assert.NoError(t, err)
    97  	assert.Len(t, licenseViolations, 1)
    98  	assert.Len(t, securityViolationsRows, 2)
    99  	assert.Equal(t, "XRAY-2", securityViolationsRows[0].IssueId)
   100  	assert.Equal(t, "low", securityViolationsRows[0].Severity)
   101  	assert.Equal(t, "XRAY-2", securityViolationsRows[1].IssueId)
   102  	assert.Equal(t, "low", securityViolationsRows[1].Severity)
   103  	assert.Equal(t, "MIT", licenseViolations[0].LicenseKey)
   104  	assert.Equal(t, "Dep-1", licenseViolations[0].ImpactedDependencyName)
   105  
   106  	impactedPackageOne := securityViolationsRows[0].ImpactedDependencyName
   107  	impactedPackageTwo := securityViolationsRows[1].ImpactedDependencyName
   108  	assert.ElementsMatch(t, []string{"component-C", "component-D"}, []string{impactedPackageOne, impactedPackageTwo})
   109  }
   110  
   111  func TestCreateVulnerabilitiesRowsCaseNoPrevViolations(t *testing.T) {
   112  	// Previous scan with no violation
   113  	previousScan := services.ScanResponse{
   114  		Violations: []services.Violation{},
   115  	}
   116  
   117  	// Current scan with 2 violations - XRAY-1 and XRAY-2
   118  	currentScan := services.ScanResponse{
   119  		Violations: []services.Violation{
   120  			{
   121  				IssueId:       "XRAY-1",
   122  				Summary:       "summary-1",
   123  				Severity:      "high",
   124  				ViolationType: "security",
   125  				Components:    map[string]services.Component{"component-A": {}},
   126  			},
   127  			{
   128  				IssueId:       "XRAY-2",
   129  				Summary:       "summary-2",
   130  				ViolationType: "security",
   131  				Severity:      "low",
   132  				Components:    map[string]services.Component{"component-C": {}},
   133  			},
   134  			{
   135  				IssueId:       "XRAY-3",
   136  				ViolationType: "license",
   137  				LicenseKey:    "MIT",
   138  				Components:    map[string]services.Component{"Dep-1": {}},
   139  			},
   140  		},
   141  	}
   142  
   143  	expectedVulns := []formats.VulnerabilityOrViolationRow{
   144  		{
   145  			IssueId: "XRAY-1",
   146  			Summary: "summary-1",
   147  			ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
   148  				SeverityDetails:        formats.SeverityDetails{Severity: "high"},
   149  				ImpactedDependencyName: "component-A",
   150  			},
   151  		},
   152  		{
   153  			IssueId: "XRAY-2",
   154  			Summary: "summary-2",
   155  			ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
   156  				SeverityDetails:        formats.SeverityDetails{Severity: "low"},
   157  				ImpactedDependencyName: "component-C",
   158  			},
   159  		},
   160  	}
   161  
   162  	expectedLicenses := []formats.LicenseRow{
   163  		{
   164  			ImpactedDependencyDetails: formats.ImpactedDependencyDetails{ImpactedDependencyName: "Dep-1"},
   165  			LicenseKey:                "MIT",
   166  		},
   167  	}
   168  
   169  	// Run createNewIssuesRows and expect both XRAY-1 and XRAY-2 violation in the results
   170  	vulnerabilities, licenses, err := createNewVulnerabilitiesRows(
   171  		&xrayutils.Results{ScaResults: []xrayutils.ScaScanResult{{XrayResults: []services.ScanResponse{previousScan}}}, ExtendedScanResults: &xrayutils.ExtendedScanResults{}},
   172  		&xrayutils.Results{ScaResults: []xrayutils.ScaScanResult{{XrayResults: []services.ScanResponse{currentScan}}}, ExtendedScanResults: &xrayutils.ExtendedScanResults{}},
   173  		[]string{},
   174  	)
   175  	assert.NoError(t, err)
   176  	assert.Len(t, licenses, 1)
   177  	assert.Len(t, vulnerabilities, 2)
   178  	assert.ElementsMatch(t, expectedVulns, vulnerabilities)
   179  	assert.Equal(t, expectedLicenses[0].ImpactedDependencyName, licenses[0].ImpactedDependencyName)
   180  	assert.Equal(t, expectedLicenses[0].LicenseKey, licenses[0].LicenseKey)
   181  }
   182  
   183  func TestGetNewViolationsCaseNoNewViolations(t *testing.T) {
   184  	// Previous scan with 2 security violations and 1 license violation - XRAY-1 and XRAY-2
   185  	previousScan := services.ScanResponse{
   186  		Violations: []services.Violation{
   187  			{
   188  				IssueId:       "XRAY-1",
   189  				Severity:      "high",
   190  				ViolationType: "security",
   191  				Components:    map[string]services.Component{"component-A": {}},
   192  			},
   193  			{
   194  				IssueId:       "XRAY-2",
   195  				Summary:       "summary-2",
   196  				ViolationType: "security",
   197  				Severity:      "low",
   198  				Components:    map[string]services.Component{"component-C": {}},
   199  			},
   200  			{
   201  				IssueId:       "XRAY-3",
   202  				LicenseKey:    "MIT",
   203  				ViolationType: "license",
   204  				Components:    map[string]services.Component{"component-B": {}},
   205  			},
   206  		},
   207  	}
   208  
   209  	// Current scan with no violation
   210  	currentScan := services.ScanResponse{
   211  		Violations: []services.Violation{},
   212  	}
   213  
   214  	// Run createNewIssuesRows and expect no violations in the results
   215  	securityViolations, licenseViolations, err := createNewVulnerabilitiesRows(
   216  		&xrayutils.Results{ScaResults: []xrayutils.ScaScanResult{{XrayResults: []services.ScanResponse{previousScan}}}, ExtendedScanResults: &xrayutils.ExtendedScanResults{}},
   217  		&xrayutils.Results{ScaResults: []xrayutils.ScaScanResult{{XrayResults: []services.ScanResponse{currentScan}}}, ExtendedScanResults: &xrayutils.ExtendedScanResults{}},
   218  		[]string{"MIT"},
   219  	)
   220  	assert.NoError(t, err)
   221  	assert.Len(t, securityViolations, 0)
   222  	assert.Len(t, licenseViolations, 0)
   223  }
   224  
   225  func TestGetNewVulnerabilities(t *testing.T) {
   226  	// Previous scan with only one vulnerability - XRAY-1
   227  	previousScan := services.ScanResponse{
   228  		Vulnerabilities: []services.Vulnerability{{
   229  			IssueId:    "XRAY-1",
   230  			Summary:    "summary-1",
   231  			Severity:   "high",
   232  			Cves:       []services.Cve{{Id: "CVE-2023-1234"}},
   233  			Components: map[string]services.Component{"component-A": {}, "component-B": {}},
   234  			Technology: coreutils.Maven.String(),
   235  		}},
   236  	}
   237  
   238  	// Current scan with 2 vulnerabilities - XRAY-1 and XRAY-2
   239  	currentScan := services.ScanResponse{
   240  		Vulnerabilities: []services.Vulnerability{
   241  			{
   242  				IssueId:    "XRAY-1",
   243  				Summary:    "summary-1",
   244  				Severity:   "high",
   245  				Cves:       []services.Cve{{Id: "CVE-2023-1234"}},
   246  				Components: map[string]services.Component{"component-A": {}, "component-B": {}},
   247  				Technology: coreutils.Maven.String(),
   248  			},
   249  			{
   250  				IssueId:    "XRAY-2",
   251  				Summary:    "summary-2",
   252  				Severity:   "low",
   253  				Cves:       []services.Cve{{Id: "CVE-2023-4321"}},
   254  				Components: map[string]services.Component{"component-C": {}, "component-D": {}},
   255  				Technology: coreutils.Yarn.String(),
   256  			},
   257  		},
   258  	}
   259  
   260  	expected := []formats.VulnerabilityOrViolationRow{
   261  		{
   262  			Summary:    "summary-2",
   263  			Applicable: "Applicable",
   264  			IssueId:    "XRAY-2",
   265  			ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
   266  				SeverityDetails:        formats.SeverityDetails{Severity: "low"},
   267  				ImpactedDependencyName: "component-C",
   268  			},
   269  			Cves:       []formats.CveRow{{Id: "CVE-2023-4321", Applicability: &formats.Applicability{Status: "Applicable", Evidence: []formats.Evidence{{Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}}}}}},
   270  			Technology: coreutils.Yarn,
   271  		},
   272  		{
   273  			Summary:    "summary-2",
   274  			Applicable: "Applicable",
   275  			IssueId:    "XRAY-2",
   276  			ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
   277  				SeverityDetails:        formats.SeverityDetails{Severity: "low"},
   278  				ImpactedDependencyName: "component-D",
   279  			},
   280  			Cves:       []formats.CveRow{{Id: "CVE-2023-4321", Applicability: &formats.Applicability{Status: "Applicable", Evidence: []formats.Evidence{{Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}}}}}},
   281  			Technology: coreutils.Yarn,
   282  		},
   283  	}
   284  
   285  	// Run createNewIssuesRows and make sure that only the XRAY-2 vulnerability exists in the results
   286  	vulnerabilities, licenses, err := createNewVulnerabilitiesRows(
   287  		&xrayutils.Results{
   288  			ScaResults: []xrayutils.ScaScanResult{{XrayResults: []services.ScanResponse{previousScan}}},
   289  			ExtendedScanResults: &xrayutils.ExtendedScanResults{
   290  				EntitledForJas:           true,
   291  				ApplicabilityScanResults: []*sarif.Run{xrayutils.CreateRunWithDummyResults(xrayutils.CreateResultWithOneLocation("file1", 1, 10, 2, 11, "snippet", "applic_CVE-2023-4321", ""))},
   292  			},
   293  		},
   294  		&xrayutils.Results{
   295  			ScaResults: []xrayutils.ScaScanResult{{XrayResults: []services.ScanResponse{currentScan}}},
   296  			ExtendedScanResults: &xrayutils.ExtendedScanResults{
   297  				EntitledForJas:           true,
   298  				ApplicabilityScanResults: []*sarif.Run{xrayutils.CreateRunWithDummyResults(xrayutils.CreateResultWithOneLocation("file1", 1, 10, 2, 11, "snippet", "applic_CVE-2023-4321", ""))},
   299  			},
   300  		},
   301  		nil,
   302  	)
   303  	assert.NoError(t, err)
   304  	assert.Len(t, vulnerabilities, 2)
   305  	assert.Len(t, licenses, 0)
   306  	assert.ElementsMatch(t, expected, vulnerabilities)
   307  }
   308  
   309  func TestGetNewVulnerabilitiesCaseNoPrevVulnerabilities(t *testing.T) {
   310  	// Previous scan with no vulnerabilities
   311  	previousScan := services.ScanResponse{
   312  		Vulnerabilities: []services.Vulnerability{},
   313  	}
   314  
   315  	// Current scan with 2 vulnerabilities - XRAY-1 and XRAY-2
   316  	currentScan := services.ScanResponse{
   317  		Vulnerabilities: []services.Vulnerability{
   318  			{
   319  				IssueId:             "XRAY-1",
   320  				Summary:             "summary-1",
   321  				Severity:            "high",
   322  				ExtendedInformation: &services.ExtendedInformation{FullDescription: "description-1"},
   323  				Components:          map[string]services.Component{"component-A": {}},
   324  			},
   325  			{
   326  				IssueId:             "XRAY-2",
   327  				Summary:             "summary-2",
   328  				Severity:            "low",
   329  				ExtendedInformation: &services.ExtendedInformation{FullDescription: "description-2"},
   330  				Components:          map[string]services.Component{"component-B": {}},
   331  			},
   332  		},
   333  	}
   334  
   335  	expected := []formats.VulnerabilityOrViolationRow{
   336  		{
   337  			Summary: "summary-2",
   338  			IssueId: "XRAY-2",
   339  			ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
   340  				SeverityDetails:        formats.SeverityDetails{Severity: "low"},
   341  				ImpactedDependencyName: "component-B",
   342  			},
   343  			JfrogResearchInformation: &formats.JfrogResearchInformation{Details: "description-2"},
   344  		},
   345  		{
   346  			Summary: "summary-1",
   347  			IssueId: "XRAY-1",
   348  			ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
   349  				SeverityDetails:        formats.SeverityDetails{Severity: "high"},
   350  				ImpactedDependencyName: "component-A",
   351  			},
   352  			JfrogResearchInformation: &formats.JfrogResearchInformation{Details: "description-1"},
   353  		},
   354  	}
   355  
   356  	// Run createNewIssuesRows and expect both XRAY-1 and XRAY-2 vulnerability in the results
   357  	vulnerabilities, licenses, err := createNewVulnerabilitiesRows(
   358  		&xrayutils.Results{ScaResults: []xrayutils.ScaScanResult{{XrayResults: []services.ScanResponse{previousScan}}}, ExtendedScanResults: &xrayutils.ExtendedScanResults{}},
   359  		&xrayutils.Results{ScaResults: []xrayutils.ScaScanResult{{XrayResults: []services.ScanResponse{currentScan}}}, ExtendedScanResults: &xrayutils.ExtendedScanResults{}},
   360  		nil,
   361  	)
   362  	assert.NoError(t, err)
   363  	assert.Len(t, vulnerabilities, 2)
   364  	assert.Len(t, licenses, 0)
   365  	assert.ElementsMatch(t, expected, vulnerabilities)
   366  }
   367  
   368  func TestGetNewVulnerabilitiesCaseNoNewVulnerabilities(t *testing.T) {
   369  	// Previous scan with 2 vulnerabilities - XRAY-1 and XRAY-2
   370  	previousScan := services.ScanResponse{
   371  		Vulnerabilities: []services.Vulnerability{
   372  			{
   373  				IssueId:    "XRAY-1",
   374  				Summary:    "summary-1",
   375  				Severity:   "high",
   376  				Components: map[string]services.Component{"component-A": {}},
   377  			},
   378  			{
   379  				IssueId:    "XRAY-2",
   380  				Summary:    "summary-2",
   381  				Severity:   "low",
   382  				Components: map[string]services.Component{"component-B": {}},
   383  			},
   384  		},
   385  	}
   386  
   387  	// Current scan with no vulnerabilities
   388  	currentScan := services.ScanResponse{
   389  		Vulnerabilities: []services.Vulnerability{},
   390  	}
   391  
   392  	// Run createNewIssuesRows and expect no vulnerability in the results
   393  	vulnerabilities, licenses, err := createNewVulnerabilitiesRows(
   394  		&xrayutils.Results{ScaResults: []xrayutils.ScaScanResult{{XrayResults: []services.ScanResponse{previousScan}}}, ExtendedScanResults: &xrayutils.ExtendedScanResults{}},
   395  		&xrayutils.Results{ScaResults: []xrayutils.ScaScanResult{{XrayResults: []services.ScanResponse{currentScan}}}, ExtendedScanResults: &xrayutils.ExtendedScanResults{}},
   396  		nil,
   397  	)
   398  	assert.NoError(t, err)
   399  	assert.Len(t, vulnerabilities, 0)
   400  	assert.Len(t, licenses, 0)
   401  }
   402  
   403  func TestGetAllIssues(t *testing.T) {
   404  	allowedLicenses := []string{"MIT"}
   405  	auditResults := &xrayutils.Results{
   406  		ScaResults: []xrayutils.ScaScanResult{{
   407  			XrayResults: []services.ScanResponse{{
   408  				Vulnerabilities: []services.Vulnerability{
   409  					{Cves: []services.Cve{{Id: "CVE-2022-2122"}}, Severity: "High", Components: map[string]services.Component{"Dep-1": {FixedVersions: []string{"1.2.3"}}}},
   410  					{Cves: []services.Cve{{Id: "CVE-2023-3122"}}, Severity: "Low", Components: map[string]services.Component{"Dep-2": {FixedVersions: []string{"1.2.2"}}}},
   411  				},
   412  				Licenses: []services.License{{Key: "Apache-2.0", Components: map[string]services.Component{"Dep-1": {FixedVersions: []string{"1.2.3"}}}}},
   413  			}},
   414  		}},
   415  		ExtendedScanResults: &xrayutils.ExtendedScanResults{
   416  			ApplicabilityScanResults: []*sarif.Run{
   417  				xrayutils.CreateRunWithDummyResults(
   418  					xrayutils.CreateDummyPassingResult("applic_CVE-2023-3122"),
   419  					xrayutils.CreateResultWithOneLocation("file1", 1, 10, 2, 11, "snippet", "applic_CVE-2022-2122", ""),
   420  				),
   421  			},
   422  			IacScanResults: []*sarif.Run{
   423  				xrayutils.CreateRunWithDummyResults(
   424  					xrayutils.CreateResultWithLocations("Missing auto upgrade was detected", "rule", xrayutils.ConvertToSarifLevel("high"),
   425  						xrayutils.CreateLocation("file1", 1, 10, 2, 11, "aws-violation"),
   426  					),
   427  				),
   428  			},
   429  			SecretsScanResults: []*sarif.Run{
   430  				xrayutils.CreateRunWithDummyResults(
   431  					xrayutils.CreateResultWithLocations("Secret", "rule", xrayutils.ConvertToSarifLevel("high"),
   432  						xrayutils.CreateLocation("index.js", 5, 6, 7, 8, "access token exposed"),
   433  					),
   434  				),
   435  			},
   436  			SastScanResults: []*sarif.Run{
   437  				xrayutils.CreateRunWithDummyResults(
   438  					xrayutils.CreateResultWithLocations("XSS Vulnerability", "rule", xrayutils.ConvertToSarifLevel("high"),
   439  						xrayutils.CreateLocation("file1", 1, 10, 2, 11, "snippet"),
   440  					),
   441  				),
   442  			},
   443  			EntitledForJas: true,
   444  		},
   445  	}
   446  	expectedOutput := &utils.IssuesCollection{
   447  		Vulnerabilities: []formats.VulnerabilityOrViolationRow{
   448  			{
   449  				Applicable:    "Applicable",
   450  				FixedVersions: []string{"1.2.3"},
   451  				ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
   452  					SeverityDetails:        formats.SeverityDetails{Severity: "High", SeverityNumValue: 13},
   453  					ImpactedDependencyName: "Dep-1",
   454  				},
   455  				Cves: []formats.CveRow{{Id: "CVE-2022-2122", Applicability: &formats.Applicability{Status: "Applicable", Evidence: []formats.Evidence{{Location: formats.Location{File: "file1", StartLine: 1, StartColumn: 10, EndLine: 2, EndColumn: 11, Snippet: "snippet"}}}}}},
   456  			},
   457  			{
   458  				Applicable:    "Not Applicable",
   459  				FixedVersions: []string{"1.2.2"},
   460  				ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
   461  					SeverityDetails:        formats.SeverityDetails{Severity: "Low", SeverityNumValue: 2},
   462  					ImpactedDependencyName: "Dep-2",
   463  				},
   464  				Cves: []formats.CveRow{{Id: "CVE-2023-3122", Applicability: &formats.Applicability{Status: "Not Applicable"}}},
   465  			},
   466  		},
   467  		Iacs: []formats.SourceCodeRow{
   468  			{
   469  				SeverityDetails: formats.SeverityDetails{
   470  					Severity:         "High",
   471  					SeverityNumValue: 13,
   472  				},
   473  				Finding: "Missing auto upgrade was detected",
   474  				Location: formats.Location{
   475  					File:        "file1",
   476  					StartLine:   1,
   477  					StartColumn: 10,
   478  					EndLine:     2,
   479  					EndColumn:   11,
   480  					Snippet:     "aws-violation",
   481  				},
   482  			},
   483  		},
   484  		Secrets: []formats.SourceCodeRow{
   485  			{
   486  				SeverityDetails: formats.SeverityDetails{
   487  					Severity:         "High",
   488  					SeverityNumValue: 13,
   489  				},
   490  				Finding: "Secret",
   491  				Location: formats.Location{
   492  					File:        "index.js",
   493  					StartLine:   5,
   494  					StartColumn: 6,
   495  					EndLine:     7,
   496  					EndColumn:   8,
   497  					Snippet:     "access token exposed",
   498  				},
   499  			},
   500  		},
   501  		Sast: []formats.SourceCodeRow{
   502  			{
   503  				SeverityDetails: formats.SeverityDetails{
   504  					Severity:         "High",
   505  					SeverityNumValue: 13,
   506  				},
   507  				Finding: "XSS Vulnerability",
   508  				Location: formats.Location{
   509  					File:        "file1",
   510  					StartLine:   1,
   511  					StartColumn: 10,
   512  					EndLine:     2,
   513  					EndColumn:   11,
   514  					Snippet:     "snippet",
   515  				},
   516  			},
   517  		},
   518  		Licenses: []formats.LicenseRow{
   519  			{
   520  				LicenseKey: "Apache-2.0",
   521  				ImpactedDependencyDetails: formats.ImpactedDependencyDetails{
   522  					ImpactedDependencyName: "Dep-1",
   523  				},
   524  			},
   525  		},
   526  	}
   527  
   528  	issuesRows, err := getAllIssues(auditResults, allowedLicenses)
   529  
   530  	if assert.NoError(t, err) {
   531  		assert.ElementsMatch(t, expectedOutput.Vulnerabilities, issuesRows.Vulnerabilities)
   532  		assert.ElementsMatch(t, expectedOutput.Iacs, issuesRows.Iacs)
   533  		assert.ElementsMatch(t, expectedOutput.Secrets, issuesRows.Secrets)
   534  		assert.ElementsMatch(t, expectedOutput.Sast, issuesRows.Sast)
   535  		assert.ElementsMatch(t, expectedOutput.Licenses, issuesRows.Licenses)
   536  	}
   537  }
   538  
   539  func TestScanPullRequest(t *testing.T) {
   540  	tests := []struct {
   541  		testName             string
   542  		configPath           string
   543  		projectName          string
   544  		failOnSecurityIssues bool
   545  	}{
   546  		{
   547  			testName:             "ScanPullRequest",
   548  			configPath:           testProjConfigPath,
   549  			projectName:          "test-proj",
   550  			failOnSecurityIssues: true,
   551  		},
   552  		{
   553  			testName:             "ScanPullRequestNoFail",
   554  			configPath:           testProjConfigPathNoFail,
   555  			projectName:          "test-proj",
   556  			failOnSecurityIssues: false,
   557  		},
   558  		{
   559  			testName:             "ScanPullRequestSubdir",
   560  			configPath:           testProjSubdirConfigPath,
   561  			projectName:          "test-proj-subdir",
   562  			failOnSecurityIssues: true,
   563  		},
   564  		{
   565  			testName:             "ScanPullRequestNoIssues",
   566  			configPath:           testCleanProjConfigPath,
   567  			projectName:          "clean-test-proj",
   568  			failOnSecurityIssues: false,
   569  		},
   570  		{
   571  			testName:             "ScanPullRequestMultiWorkDir",
   572  			configPath:           testMultiDirProjConfigPathNoFail,
   573  			projectName:          "multi-dir-test-proj",
   574  			failOnSecurityIssues: false,
   575  		},
   576  		{
   577  			testName:             "ScanPullRequestMultiWorkDirNoFail",
   578  			configPath:           testMultiDirProjConfigPath,
   579  			projectName:          "multi-dir-test-proj",
   580  			failOnSecurityIssues: true,
   581  		},
   582  	}
   583  	for _, test := range tests {
   584  		t.Run(test.testName, func(t *testing.T) {
   585  			testScanPullRequest(t, test.configPath, test.projectName, test.failOnSecurityIssues)
   586  		})
   587  	}
   588  }
   589  
   590  func testScanPullRequest(t *testing.T, configPath, projectName string, failOnSecurityIssues bool) {
   591  	params, restoreEnv := utils.VerifyEnv(t)
   592  	defer restoreEnv()
   593  
   594  	// Create mock GitLab server
   595  	server := httptest.NewServer(createGitLabHandler(t, projectName))
   596  	defer server.Close()
   597  
   598  	configAggregator, client := prepareConfigAndClient(t, configPath, server, params)
   599  	testDir, cleanUp := utils.CopyTestdataProjectsToTemp(t, "scanpullrequest")
   600  	defer cleanUp()
   601  
   602  	// Renames test git folder to .git
   603  	currentDir := filepath.Join(testDir, projectName)
   604  	restoreDir, err := utils.Chdir(currentDir)
   605  	assert.NoError(t, err)
   606  	defer func() {
   607  		assert.NoError(t, restoreDir())
   608  		assert.NoError(t, fileutils.RemoveTempDir(currentDir))
   609  	}()
   610  
   611  	// Run "frogbot scan pull request"
   612  	var scanPullRequest ScanPullRequestCmd
   613  	err = scanPullRequest.Run(configAggregator, client, utils.MockHasConnection())
   614  	if failOnSecurityIssues {
   615  		assert.EqualErrorf(t, err, SecurityIssueFoundErr, "Error should be: %v, got: %v", SecurityIssueFoundErr, err)
   616  	} else {
   617  		assert.NoError(t, err)
   618  	}
   619  
   620  	// Check env sanitize
   621  	err = utils.SanitizeEnv()
   622  	assert.NoError(t, err)
   623  	utils.AssertSanitizedEnv(t)
   624  }
   625  
   626  func TestVerifyGitHubFrogbotEnvironment(t *testing.T) {
   627  	// Init mock
   628  	client := CreateMockVcsClient(t)
   629  	environment := "frogbot"
   630  	client.EXPECT().GetRepositoryInfo(context.Background(), gitParams.RepoOwner, gitParams.RepoName).Return(vcsclient.RepositoryInfo{}, nil)
   631  	client.EXPECT().GetRepositoryEnvironmentInfo(context.Background(), gitParams.RepoOwner, gitParams.RepoName, environment).Return(vcsclient.RepositoryEnvironmentInfo{Reviewers: []string{"froggy"}}, nil)
   632  	assert.NoError(t, os.Setenv(utils.GitHubActionsEnv, "true"))
   633  
   634  	// Run verifyGitHubFrogbotEnvironment
   635  	err := verifyGitHubFrogbotEnvironment(client, gitParams)
   636  	assert.NoError(t, err)
   637  }
   638  
   639  func TestVerifyGitHubFrogbotEnvironmentNoEnv(t *testing.T) {
   640  	// Redirect log to avoid negative output
   641  	previousLogger := redirectLogOutputToNil()
   642  	defer log.SetLogger(previousLogger)
   643  
   644  	// Init mock
   645  	client := CreateMockVcsClient(t)
   646  	environment := "frogbot"
   647  	client.EXPECT().GetRepositoryInfo(context.Background(), gitParams.RepoOwner, gitParams.RepoName).Return(vcsclient.RepositoryInfo{}, nil)
   648  	client.EXPECT().GetRepositoryEnvironmentInfo(context.Background(), gitParams.RepoOwner, gitParams.RepoName, environment).Return(vcsclient.RepositoryEnvironmentInfo{}, errors.New("404"))
   649  	assert.NoError(t, os.Setenv(utils.GitHubActionsEnv, "true"))
   650  
   651  	// Run verifyGitHubFrogbotEnvironment
   652  	err := verifyGitHubFrogbotEnvironment(client, gitParams)
   653  	assert.ErrorContains(t, err, noGitHubEnvErr)
   654  }
   655  
   656  func TestVerifyGitHubFrogbotEnvironmentNoReviewers(t *testing.T) {
   657  	// Init mock
   658  	client := CreateMockVcsClient(t)
   659  	environment := "frogbot"
   660  	client.EXPECT().GetRepositoryInfo(context.Background(), gitParams.RepoOwner, gitParams.RepoName).Return(vcsclient.RepositoryInfo{}, nil)
   661  	client.EXPECT().GetRepositoryEnvironmentInfo(context.Background(), gitParams.RepoOwner, gitParams.RepoName, environment).Return(vcsclient.RepositoryEnvironmentInfo{}, nil)
   662  	assert.NoError(t, os.Setenv(utils.GitHubActionsEnv, "true"))
   663  
   664  	// Run verifyGitHubFrogbotEnvironment
   665  	err := verifyGitHubFrogbotEnvironment(client, gitParams)
   666  	assert.ErrorContains(t, err, noGitHubEnvReviewersErr)
   667  }
   668  
   669  func TestVerifyGitHubFrogbotEnvironmentOnPrem(t *testing.T) {
   670  	repoConfig := &utils.Repository{
   671  		Params: utils.Params{Git: utils.Git{
   672  			VcsInfo: vcsclient.VcsInfo{APIEndpoint: "https://acme.vcs.io"}},
   673  		},
   674  	}
   675  
   676  	// Run verifyGitHubFrogbotEnvironment
   677  	err := verifyGitHubFrogbotEnvironment(&vcsclient.GitHubClient{}, repoConfig)
   678  	assert.NoError(t, err)
   679  }
   680  
   681  func prepareConfigAndClient(t *testing.T, configPath string, server *httptest.Server, serverParams coreconfig.ServerDetails) (utils.RepoAggregator, vcsclient.VcsClient) {
   682  	gitTestParams := &utils.Git{
   683  		GitProvider: vcsutils.GitHub,
   684  		RepoOwner:   "jfrog",
   685  		VcsInfo: vcsclient.VcsInfo{
   686  			Token:       "123456",
   687  			APIEndpoint: server.URL,
   688  		},
   689  		PullRequestDetails: vcsclient.PullRequestInfo{ID: int64(1)},
   690  	}
   691  	utils.SetEnvAndAssert(t, map[string]string{utils.GitPullRequestIDEnv: "1"})
   692  
   693  	configData, err := utils.ReadConfigFromFileSystem(configPath)
   694  	assert.NoError(t, err)
   695  	configAggregator, err := utils.BuildRepoAggregator(configData, gitTestParams, &serverParams, utils.ScanPullRequest)
   696  	assert.NoError(t, err)
   697  
   698  	client, err := vcsclient.NewClientBuilder(vcsutils.GitLab).ApiEndpoint(server.URL).Token("123456").Build()
   699  	assert.NoError(t, err)
   700  	return configAggregator, client
   701  }
   702  
   703  // Create HTTP handler to mock GitLab server
   704  func createGitLabHandler(t *testing.T, projectName string) http.HandlerFunc {
   705  	return func(w http.ResponseWriter, r *http.Request) {
   706  		switch {
   707  		// Return 200 on ping
   708  		case r.RequestURI == "/api/v4/":
   709  			w.WriteHeader(http.StatusOK)
   710  		// Mimic get pull request by ID
   711  		case r.RequestURI == fmt.Sprintf("/api/v4/projects/jfrog%s/merge_requests/1", "%2F"+projectName):
   712  			w.WriteHeader(http.StatusOK)
   713  			expectedResponse, err := os.ReadFile(filepath.Join("..", "expectedPullRequestDetailsResponse.json"))
   714  			assert.NoError(t, err)
   715  			_, err = w.Write(expectedResponse)
   716  			assert.NoError(t, err)
   717  		// Mimic download specific branch to scan
   718  		case r.RequestURI == fmt.Sprintf("/api/v4/projects/jfrog%s/repository/archive.tar.gz?sha=%s", "%2F"+projectName, testSourceBranchName):
   719  			w.WriteHeader(http.StatusOK)
   720  			repoFile, err := os.ReadFile(filepath.Join("..", projectName, "sourceBranch.gz"))
   721  			assert.NoError(t, err)
   722  			_, err = w.Write(repoFile)
   723  			assert.NoError(t, err)
   724  		// Download repository mock
   725  		case r.RequestURI == fmt.Sprintf("/api/v4/projects/jfrog%s/repository/archive.tar.gz?sha=%s", "%2F"+projectName, testTargetBranchName):
   726  			w.WriteHeader(http.StatusOK)
   727  			repoFile, err := os.ReadFile(filepath.Join("..", projectName, "targetBranch.gz"))
   728  			assert.NoError(t, err)
   729  			_, err = w.Write(repoFile)
   730  			assert.NoError(t, err)
   731  			return
   732  		// clean-test-proj should not include any vulnerabilities so assertion is not needed.
   733  		case r.RequestURI == fmt.Sprintf("/api/v4/projects/jfrog%s/merge_requests/133/notes", "%2Fclean-test-proj") && r.Method == http.MethodPost:
   734  			w.WriteHeader(http.StatusOK)
   735  			_, err := w.Write([]byte("{}"))
   736  			assert.NoError(t, err)
   737  			return
   738  		case r.RequestURI == fmt.Sprintf("/api/v4/projects/jfrog%s/merge_requests/133/notes", "%2Fclean-test-proj") && r.Method == http.MethodGet:
   739  			w.WriteHeader(http.StatusOK)
   740  			comments, err := os.ReadFile(filepath.Join("..", "commits.json"))
   741  			assert.NoError(t, err)
   742  			_, err = w.Write(comments)
   743  			assert.NoError(t, err)
   744  		// Return 200 when using the REST that creates the comment
   745  		case r.RequestURI == fmt.Sprintf("/api/v4/projects/jfrog%s/merge_requests/133/notes", "%2F"+projectName) && r.Method == http.MethodPost:
   746  			buf := new(bytes.Buffer)
   747  			_, err := buf.ReadFrom(r.Body)
   748  			assert.NoError(t, err)
   749  			assert.NotEmpty(t, buf.String())
   750  
   751  			var expectedResponse []byte
   752  			if strings.Contains(projectName, "multi-dir") {
   753  				expectedResponse = outputwriter.GetJsonBodyOutputFromFile(t, filepath.Join("..", "expected_response_multi_dir.md"))
   754  			} else {
   755  				expectedResponse = outputwriter.GetJsonBodyOutputFromFile(t, filepath.Join("..", "expected_response.md"))
   756  			}
   757  			assert.NoError(t, err)
   758  			assert.JSONEq(t, string(expectedResponse), buf.String())
   759  
   760  			w.WriteHeader(http.StatusOK)
   761  			_, err = w.Write([]byte("{}"))
   762  			assert.NoError(t, err)
   763  		case r.RequestURI == fmt.Sprintf("/api/v4/projects/jfrog%s/merge_requests/133/notes", "%2F"+projectName) && r.Method == http.MethodGet:
   764  			w.WriteHeader(http.StatusOK)
   765  			comments, err := os.ReadFile(filepath.Join("..", "commits.json"))
   766  			assert.NoError(t, err)
   767  			_, err = w.Write(comments)
   768  			assert.NoError(t, err)
   769  		case r.RequestURI == fmt.Sprintf("/api/v4/projects/jfrog%s", "%2F"+projectName):
   770  			jsonResponse := `{"id": 3,"visibility": "private","ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git","http_url_to_repo": "https://example.com/diaspora/diaspora-project-site.git"}`
   771  			_, err := w.Write([]byte(jsonResponse))
   772  			assert.NoError(t, err)
   773  		case r.RequestURI == fmt.Sprintf("/api/v4/projects/jfrog%s/merge_requests/133/discussions", "%2F"+projectName):
   774  			discussions, err := os.ReadFile(filepath.Join("..", "list_merge_request_discussion_items.json"))
   775  			assert.NoError(t, err)
   776  			_, err = w.Write(discussions)
   777  			assert.NoError(t, err)
   778  		}
   779  	}
   780  }
   781  
   782  func TestCreateNewIacRows(t *testing.T) {
   783  	testCases := []struct {
   784  		name                            string
   785  		targetIacResults                []*sarif.Result
   786  		sourceIacResults                []*sarif.Result
   787  		expectedAddedIacVulnerabilities []formats.SourceCodeRow
   788  	}{
   789  		{
   790  			name: "No vulnerabilities in source IaC results",
   791  			targetIacResults: []*sarif.Result{
   792  				xrayutils.CreateResultWithLocations("Missing auto upgrade was detected", "rule", xrayutils.ConvertToSarifLevel("high"),
   793  					xrayutils.CreateLocation("file1", 1, 10, 2, 11, "aws-violation"),
   794  				),
   795  			},
   796  			sourceIacResults:                []*sarif.Result{},
   797  			expectedAddedIacVulnerabilities: []formats.SourceCodeRow{},
   798  		},
   799  		{
   800  			name:             "No vulnerabilities in target IaC results",
   801  			targetIacResults: []*sarif.Result{},
   802  			sourceIacResults: []*sarif.Result{
   803  				xrayutils.CreateResultWithLocations("Missing auto upgrade was detected", "rule", xrayutils.ConvertToSarifLevel("high"),
   804  					xrayutils.CreateLocation("file1", 1, 10, 2, 11, "aws-violation"),
   805  				),
   806  			},
   807  			expectedAddedIacVulnerabilities: []formats.SourceCodeRow{
   808  				{
   809  					SeverityDetails: formats.SeverityDetails{
   810  						Severity:         "High",
   811  						SeverityNumValue: 13,
   812  					},
   813  					Finding: "Missing auto upgrade was detected",
   814  					Location: formats.Location{
   815  						File:        "file1",
   816  						StartLine:   1,
   817  						StartColumn: 10,
   818  						EndLine:     2,
   819  						EndColumn:   11,
   820  						Snippet:     "aws-violation",
   821  					},
   822  				},
   823  			},
   824  		},
   825  		{
   826  			name: "Some new vulnerabilities in source IaC results",
   827  			targetIacResults: []*sarif.Result{
   828  				xrayutils.CreateResultWithLocations("Missing auto upgrade was detected", "rule", xrayutils.ConvertToSarifLevel("high"),
   829  					xrayutils.CreateLocation("file1", 1, 10, 2, 11, "aws-violation"),
   830  				),
   831  			},
   832  			sourceIacResults: []*sarif.Result{
   833  				xrayutils.CreateResultWithLocations("enable_private_endpoint=false was detected", "rule", xrayutils.ConvertToSarifLevel("medium"),
   834  					xrayutils.CreateLocation("file2", 2, 5, 3, 6, "gcp-violation"),
   835  				),
   836  			},
   837  			expectedAddedIacVulnerabilities: []formats.SourceCodeRow{
   838  				{
   839  					SeverityDetails: formats.SeverityDetails{
   840  						Severity:         "Medium",
   841  						SeverityNumValue: 11,
   842  					},
   843  					Finding: "enable_private_endpoint=false was detected",
   844  					Location: formats.Location{
   845  						File:        "file2",
   846  						StartLine:   2,
   847  						StartColumn: 5,
   848  						EndLine:     3,
   849  						EndColumn:   6,
   850  						Snippet:     "gcp-violation",
   851  					},
   852  				},
   853  			},
   854  		},
   855  	}
   856  
   857  	for _, tc := range testCases {
   858  		t.Run(tc.name, func(t *testing.T) {
   859  			targetIacRows := xrayutils.PrepareIacs([]*sarif.Run{xrayutils.CreateRunWithDummyResults(tc.targetIacResults...)})
   860  			sourceIacRows := xrayutils.PrepareIacs([]*sarif.Run{xrayutils.CreateRunWithDummyResults(tc.sourceIacResults...)})
   861  			addedIacVulnerabilities := createNewSourceCodeRows(targetIacRows, sourceIacRows)
   862  			assert.ElementsMatch(t, tc.expectedAddedIacVulnerabilities, addedIacVulnerabilities)
   863  		})
   864  	}
   865  }
   866  
   867  func TestCreateNewSecretRows(t *testing.T) {
   868  	testCases := []struct {
   869  		name                                string
   870  		targetSecretsResults                []*sarif.Result
   871  		sourceSecretsResults                []*sarif.Result
   872  		expectedAddedSecretsVulnerabilities []formats.SourceCodeRow
   873  	}{
   874  		{
   875  			name: "No vulnerabilities in source secrets results",
   876  			targetSecretsResults: []*sarif.Result{
   877  				xrayutils.CreateResultWithLocations("Secret", "rule", xrayutils.ConvertToSarifLevel("high"),
   878  					xrayutils.CreateLocation("file1", 1, 10, 2, 11, "Sensitive information"),
   879  				),
   880  			},
   881  			sourceSecretsResults:                []*sarif.Result{},
   882  			expectedAddedSecretsVulnerabilities: []formats.SourceCodeRow{},
   883  		},
   884  		{
   885  			name:                 "No vulnerabilities in target secrets results",
   886  			targetSecretsResults: []*sarif.Result{},
   887  			sourceSecretsResults: []*sarif.Result{
   888  				xrayutils.CreateResultWithLocations("Secret", "rule", xrayutils.ConvertToSarifLevel("high"),
   889  					xrayutils.CreateLocation("file1", 1, 10, 2, 11, "Sensitive information"),
   890  				),
   891  			},
   892  			expectedAddedSecretsVulnerabilities: []formats.SourceCodeRow{
   893  				{
   894  					SeverityDetails: formats.SeverityDetails{
   895  						Severity:         "High",
   896  						SeverityNumValue: 13,
   897  					},
   898  					Finding: "Secret",
   899  					Location: formats.Location{
   900  						File:        "file1",
   901  						StartLine:   1,
   902  						StartColumn: 10,
   903  						EndLine:     2,
   904  						EndColumn:   11,
   905  						Snippet:     "Sensitive information",
   906  					},
   907  				},
   908  			},
   909  		},
   910  		{
   911  			name: "Some new vulnerabilities in source secrets results",
   912  			targetSecretsResults: []*sarif.Result{
   913  				xrayutils.CreateResultWithLocations("Secret", "rule", xrayutils.ConvertToSarifLevel("high"),
   914  					xrayutils.CreateLocation("file1", 1, 10, 2, 11, "Sensitive information"),
   915  				),
   916  			},
   917  			sourceSecretsResults: []*sarif.Result{
   918  				xrayutils.CreateResultWithLocations("Secret", "rule", xrayutils.ConvertToSarifLevel("medium"),
   919  					xrayutils.CreateLocation("file2", 2, 5, 3, 6, "Confidential data"),
   920  				),
   921  			},
   922  			expectedAddedSecretsVulnerabilities: []formats.SourceCodeRow{
   923  				{
   924  					SeverityDetails: formats.SeverityDetails{
   925  						Severity:         "Medium",
   926  						SeverityNumValue: 11,
   927  					},
   928  					Finding: "Secret",
   929  					Location: formats.Location{
   930  						File:        "file2",
   931  						StartLine:   2,
   932  						StartColumn: 5,
   933  						EndLine:     3,
   934  						EndColumn:   6,
   935  						Snippet:     "Confidential data",
   936  					},
   937  				},
   938  			},
   939  		},
   940  	}
   941  
   942  	for _, tc := range testCases {
   943  		t.Run(tc.name, func(t *testing.T) {
   944  			targetSecretsRows := xrayutils.PrepareSecrets([]*sarif.Run{xrayutils.CreateRunWithDummyResults(tc.targetSecretsResults...)})
   945  			sourceSecretsRows := xrayutils.PrepareSecrets([]*sarif.Run{xrayutils.CreateRunWithDummyResults(tc.sourceSecretsResults...)})
   946  			addedSecretsVulnerabilities := createNewSourceCodeRows(targetSecretsRows, sourceSecretsRows)
   947  			assert.ElementsMatch(t, tc.expectedAddedSecretsVulnerabilities, addedSecretsVulnerabilities)
   948  		})
   949  	}
   950  }
   951  
   952  func TestCreateNewSastRows(t *testing.T) {
   953  	testCases := []struct {
   954  		name                             string
   955  		targetSastResults                []*sarif.Result
   956  		sourceSastResults                []*sarif.Result
   957  		expectedAddedSastVulnerabilities []formats.SourceCodeRow
   958  	}{
   959  		{
   960  			name: "No vulnerabilities in source Sast results",
   961  			targetSastResults: []*sarif.Result{
   962  				xrayutils.CreateResultWithLocations("XSS Vulnerability", "rule", xrayutils.ConvertToSarifLevel("high"),
   963  					xrayutils.CreateLocation("file1", 1, 10, 2, 11, "snippet"),
   964  				),
   965  			},
   966  			sourceSastResults:                []*sarif.Result{},
   967  			expectedAddedSastVulnerabilities: []formats.SourceCodeRow{},
   968  		},
   969  		{
   970  			name:              "No vulnerabilities in target Sast results",
   971  			targetSastResults: []*sarif.Result{},
   972  			sourceSastResults: []*sarif.Result{
   973  				xrayutils.CreateResultWithLocations("XSS Vulnerability", "rule", xrayutils.ConvertToSarifLevel("high"),
   974  					xrayutils.CreateLocation("file1", 1, 10, 2, 11, "snippet"),
   975  				),
   976  			},
   977  			expectedAddedSastVulnerabilities: []formats.SourceCodeRow{
   978  				{
   979  					SeverityDetails: formats.SeverityDetails{
   980  						Severity:         "High",
   981  						SeverityNumValue: 13,
   982  					},
   983  					Finding: "XSS Vulnerability",
   984  					Location: formats.Location{
   985  						File:        "file1",
   986  						StartLine:   1,
   987  						StartColumn: 10,
   988  						EndLine:     2,
   989  						EndColumn:   11,
   990  						Snippet:     "snippet",
   991  					},
   992  				},
   993  			},
   994  		},
   995  		{
   996  			name: "Some new vulnerabilities in source Sast results",
   997  			targetSastResults: []*sarif.Result{
   998  				xrayutils.CreateResultWithLocations("XSS Vulnerability", "rule", xrayutils.ConvertToSarifLevel("high"),
   999  					xrayutils.CreateLocation("file1", 1, 10, 2, 11, "snippet"),
  1000  				),
  1001  			},
  1002  			sourceSastResults: []*sarif.Result{
  1003  				xrayutils.CreateResultWithLocations("Stack Trace Exposure", "rule", xrayutils.ConvertToSarifLevel("medium"),
  1004  					xrayutils.CreateLocation("file2", 2, 5, 3, 6, "other-snippet"),
  1005  				),
  1006  			},
  1007  			expectedAddedSastVulnerabilities: []formats.SourceCodeRow{
  1008  				{
  1009  					SeverityDetails: formats.SeverityDetails{
  1010  						Severity:         "Medium",
  1011  						SeverityNumValue: 11,
  1012  					},
  1013  					Finding: "Stack Trace Exposure",
  1014  					Location: formats.Location{
  1015  						File:        "file2",
  1016  						StartLine:   2,
  1017  						StartColumn: 5,
  1018  						EndLine:     3,
  1019  						EndColumn:   6,
  1020  						Snippet:     "other-snippet",
  1021  					},
  1022  				},
  1023  			},
  1024  		},
  1025  	}
  1026  
  1027  	for _, tc := range testCases {
  1028  		t.Run(tc.name, func(t *testing.T) {
  1029  			targetSastRows := xrayutils.PrepareSast([]*sarif.Run{xrayutils.CreateRunWithDummyResults(tc.targetSastResults...)})
  1030  			sourceSastRows := xrayutils.PrepareSast([]*sarif.Run{xrayutils.CreateRunWithDummyResults(tc.sourceSastResults...)})
  1031  			addedSastVulnerabilities := createNewSourceCodeRows(targetSastRows, sourceSastRows)
  1032  			assert.ElementsMatch(t, tc.expectedAddedSastVulnerabilities, addedSastVulnerabilities)
  1033  		})
  1034  	}
  1035  }
  1036  
  1037  func TestDeletePreviousPullRequestMessages(t *testing.T) {
  1038  	repository := &utils.Repository{
  1039  		Params: utils.Params{
  1040  			Git: utils.Git{
  1041  				PullRequestDetails: vcsclient.PullRequestInfo{Target: vcsclient.BranchInfo{
  1042  					Repository: "repo",
  1043  					Owner:      "owner",
  1044  				}, ID: 17},
  1045  			},
  1046  		},
  1047  		OutputWriter: &outputwriter.StandardOutput{},
  1048  	}
  1049  	client := CreateMockVcsClient(t)
  1050  
  1051  	testCases := []struct {
  1052  		name         string
  1053  		commentsOnPR []vcsclient.CommentInfo
  1054  		err          error
  1055  	}{
  1056  		{
  1057  			name: "Test with comment returned",
  1058  			commentsOnPR: []vcsclient.CommentInfo{
  1059  				{ID: 20, Content: outputwriter.GetBanner(outputwriter.NoVulnerabilityPrBannerSource) + "text \n table\n text text text", Created: time.Unix(3, 0)},
  1060  			},
  1061  		},
  1062  		{
  1063  			name: "Test with no comment returned",
  1064  		},
  1065  		{
  1066  			name: "Test with error returned",
  1067  			err:  errors.New("error"),
  1068  		},
  1069  	}
  1070  
  1071  	for _, tc := range testCases {
  1072  		t.Run(tc.name, func(t *testing.T) {
  1073  			// Test with comment returned
  1074  			client.EXPECT().ListPullRequestComments(context.Background(), "owner", "repo", 17).Return(tc.commentsOnPR, tc.err)
  1075  			client.EXPECT().DeletePullRequestComment(context.Background(), "owner", "repo", 17, 20).Return(nil).AnyTimes()
  1076  			err := utils.DeleteExistingPullRequestComments(repository, client)
  1077  			if tc.err == nil {
  1078  				assert.NoError(t, err)
  1079  			} else {
  1080  				assert.Error(t, err)
  1081  			}
  1082  		})
  1083  	}
  1084  }
  1085  
  1086  func TestDeletePreviousPullRequestReviewMessages(t *testing.T) {
  1087  	repository := &utils.Repository{
  1088  		Params: utils.Params{
  1089  			Git: utils.Git{
  1090  				PullRequestDetails: vcsclient.PullRequestInfo{Target: vcsclient.BranchInfo{
  1091  					Repository: "repo",
  1092  					Owner:      "owner",
  1093  				}, ID: 17},
  1094  			},
  1095  		},
  1096  		OutputWriter: &outputwriter.StandardOutput{},
  1097  	}
  1098  	client := CreateMockVcsClient(t)
  1099  
  1100  	testCases := []struct {
  1101  		name         string
  1102  		commentsOnPR []vcsclient.CommentInfo
  1103  		err          error
  1104  	}{
  1105  		{
  1106  			name: "Test with comment returned",
  1107  			commentsOnPR: []vcsclient.CommentInfo{
  1108  				{ID: 20, Content: outputwriter.MarkdownComment(outputwriter.ReviewCommentId) + "text \n table\n text text text", Created: time.Unix(3, 0)},
  1109  			},
  1110  		},
  1111  		{
  1112  			name: "Test with no comment returned",
  1113  		},
  1114  		{
  1115  			name: "Test with error returned",
  1116  			err:  errors.New("error"),
  1117  		},
  1118  	}
  1119  
  1120  	for _, tc := range testCases {
  1121  		t.Run(tc.name, func(t *testing.T) {
  1122  			// Test with comment returned
  1123  			client.EXPECT().ListPullRequestReviewComments(context.Background(), "", "", 17).Return(tc.commentsOnPR, tc.err)
  1124  			client.EXPECT().DeletePullRequestReviewComments(context.Background(), "", "", 17, tc.commentsOnPR).Return(nil).AnyTimes()
  1125  			err := utils.DeleteExistingPullRequestReviewComments(repository, 17, client)
  1126  			if tc.err == nil {
  1127  				assert.NoError(t, err)
  1128  			} else {
  1129  				assert.Error(t, err)
  1130  			}
  1131  		})
  1132  	}
  1133  }
  1134  
  1135  func TestAggregateScanResults(t *testing.T) {
  1136  	scanResult1 := services.ScanResponse{
  1137  		Violations:      []services.Violation{{IssueId: "Violation 1"}},
  1138  		Vulnerabilities: []services.Vulnerability{{IssueId: "Vulnerability 1"}},
  1139  		Licenses:        []services.License{{Name: "License 1"}},
  1140  	}
  1141  
  1142  	scanResult2 := services.ScanResponse{
  1143  		Violations:      []services.Violation{{IssueId: "Violation 2"}},
  1144  		Vulnerabilities: []services.Vulnerability{{IssueId: "Vulnerability 2"}},
  1145  		Licenses:        []services.License{{Name: "License 2"}},
  1146  	}
  1147  
  1148  	aggregateResult := aggregateScanResults([]services.ScanResponse{scanResult1, scanResult2})
  1149  	expectedResult := services.ScanResponse{
  1150  		Violations: []services.Violation{
  1151  			{IssueId: "Violation 1"},
  1152  			{IssueId: "Violation 2"},
  1153  		},
  1154  		Vulnerabilities: []services.Vulnerability{
  1155  			{IssueId: "Vulnerability 1"},
  1156  			{IssueId: "Vulnerability 2"},
  1157  		},
  1158  		Licenses: []services.License{
  1159  			{Name: "License 1"},
  1160  			{Name: "License 2"},
  1161  		},
  1162  	}
  1163  
  1164  	assert.Equal(t, expectedResult, aggregateResult)
  1165  }
  1166  
  1167  // Set new logger with output redirection to a null logger. This is useful for negative tests.
  1168  // Caller is responsible to set the old log back.
  1169  func redirectLogOutputToNil() (previousLog log.Log) {
  1170  	previousLog = log.Logger
  1171  	newLog := log.NewLogger(log.ERROR, nil)
  1172  	newLog.SetOutputWriter(io.Discard)
  1173  	newLog.SetLogsWriter(io.Discard, 0)
  1174  	log.SetLogger(newLog)
  1175  	return previousLog
  1176  }