github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/cmd/whitesourceExecuteScan_test.go (about)

     1  package cmd
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"path/filepath"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/SAP/jenkins-library/pkg/format"
    11  	"github.com/SAP/jenkins-library/pkg/mock"
    12  	"github.com/SAP/jenkins-library/pkg/piperutils"
    13  	"github.com/SAP/jenkins-library/pkg/reporting"
    14  	"github.com/SAP/jenkins-library/pkg/versioning"
    15  	ws "github.com/SAP/jenkins-library/pkg/whitesource"
    16  	"github.com/pkg/errors"
    17  	"github.com/stretchr/testify/assert"
    18  
    19  	"github.com/google/go-github/v45/github"
    20  )
    21  
    22  type whitesourceUtilsMock struct {
    23  	*ws.ScanUtilsMock
    24  	coordinates             versioning.Coordinates
    25  	usedBuildTool           string
    26  	usedBuildDescriptorFile string
    27  	usedOptions             versioning.Options
    28  }
    29  
    30  func (w *whitesourceUtilsMock) GetArtifactCoordinates(buildTool, buildDescriptorFile string,
    31  	options *versioning.Options,
    32  ) (versioning.Coordinates, error) {
    33  	w.usedBuildTool = buildTool
    34  	w.usedBuildDescriptorFile = buildDescriptorFile
    35  	w.usedOptions = *options
    36  	return w.coordinates, nil
    37  }
    38  
    39  const wsTimeNow = "2010-05-10 00:15:42"
    40  
    41  func (w *whitesourceUtilsMock) Now() time.Time {
    42  	now, _ := time.Parse("2006-01-02 15:04:05", wsTimeNow)
    43  	return now
    44  }
    45  
    46  func (w *whitesourceUtilsMock) GetIssueService() *github.IssuesService {
    47  	return nil
    48  }
    49  
    50  func (w *whitesourceUtilsMock) GetSearchService() *github.SearchService {
    51  	return nil
    52  }
    53  
    54  func newWhitesourceUtilsMock() *whitesourceUtilsMock {
    55  	return &whitesourceUtilsMock{
    56  		ScanUtilsMock: &ws.ScanUtilsMock{
    57  			FilesMock:      &mock.FilesMock{},
    58  			ExecMockRunner: &mock.ExecMockRunner{},
    59  		},
    60  		coordinates: versioning.Coordinates{
    61  			GroupID:    "mock-group-id",
    62  			ArtifactID: "mock-artifact-id",
    63  			Version:    "1.0.42",
    64  		},
    65  	}
    66  }
    67  
    68  func TestNewWhitesourceUtils(t *testing.T) {
    69  	t.Parallel()
    70  	config := ScanOptions{}
    71  	utils := newWhitesourceUtils(&config, &github.Client{Issues: &github.IssuesService{}, Search: &github.SearchService{}})
    72  
    73  	assert.NotNil(t, utils.Client)
    74  	assert.NotNil(t, utils.Command)
    75  	assert.NotNil(t, utils.Files)
    76  }
    77  
    78  func TestRunWhitesourceExecuteScan(t *testing.T) {
    79  	t.Parallel()
    80  	t.Run("fails for invalid configured project token", func(t *testing.T) {
    81  		ctx := context.Background()
    82  		// init
    83  		config := ScanOptions{
    84  			BuildDescriptorFile: "my-mta.yml",
    85  			VersioningModel:     "major",
    86  			ProductName:         "mock-product",
    87  			ProjectToken:        "no-such-project-token",
    88  			AgentDownloadURL:    "https://whitesource.com/agent.jar",
    89  			AgentFileName:       "ua.jar",
    90  		}
    91  		utilsMock := newWhitesourceUtilsMock()
    92  		utilsMock.AddFile("wss-generated-file.config", []byte("key=value"))
    93  		systemMock := ws.NewSystemMock("ignored")
    94  		scan := newWhitesourceScan(&config)
    95  		cpe := whitesourceExecuteScanCommonPipelineEnvironment{}
    96  		influx := whitesourceExecuteScanInflux{}
    97  		// test
    98  		err := runWhitesourceExecuteScan(ctx, &config, scan, utilsMock, systemMock, &cpe, &influx)
    99  		// assert
   100  		assert.EqualError(t, err, "failed to resolve and aggregate project name: failed to get project by token: no project with token 'no-such-project-token' found in Whitesource")
   101  		assert.Equal(t, "", config.ProjectName)
   102  		assert.Equal(t, "", scan.AggregateProjectName)
   103  	})
   104  	t.Run("retrieves aggregate project name by configured token", func(t *testing.T) {
   105  		ctx := context.Background()
   106  		// init
   107  		config := ScanOptions{
   108  			BuildDescriptorFile:       "my-mta.yml",
   109  			VersioningModel:           "major",
   110  			AgentDownloadURL:          "https://whitesource.com/agent.jar",
   111  			VulnerabilityReportFormat: "pdf",
   112  			Reporting:                 true,
   113  			AgentFileName:             "ua.jar",
   114  			ProductName:               "mock-product",
   115  			ProjectToken:              "mock-project-token",
   116  		}
   117  		utilsMock := newWhitesourceUtilsMock()
   118  		utilsMock.AddFile("wss-generated-file.config", []byte("key=value"))
   119  		lastUpdatedDate := time.Now().Format(ws.DateTimeLayout)
   120  		systemMock := ws.NewSystemMock(lastUpdatedDate)
   121  		systemMock.Alerts = []ws.Alert{}
   122  		scan := newWhitesourceScan(&config)
   123  		cpe := whitesourceExecuteScanCommonPipelineEnvironment{}
   124  		influx := whitesourceExecuteScanInflux{}
   125  		// test
   126  		err := runWhitesourceExecuteScan(ctx, &config, scan, utilsMock, systemMock, &cpe, &influx)
   127  		// assert
   128  		assert.NoError(t, err)
   129  		// Retrieved project name is stored in scan.AggregateProjectName, but not in config.ProjectName
   130  		// in order to differentiate between aggregate-project scanning and multi-project scanning.
   131  		assert.Equal(t, "", config.ProjectName)
   132  		assert.Equal(t, "mock-project", scan.AggregateProjectName)
   133  		if assert.Len(t, utilsMock.DownloadedFiles, 1) {
   134  			assert.Equal(t, ws.DownloadedFile{
   135  				SourceURL: "https://whitesource.com/agent.jar",
   136  				FilePath:  "ua.jar",
   137  			}, utilsMock.DownloadedFiles[0])
   138  		}
   139  		if assert.Len(t, cpe.custom.whitesourceProjectNames, 1) {
   140  			assert.Equal(t, []string{"mock-project - 1"}, cpe.custom.whitesourceProjectNames)
   141  		}
   142  		assert.True(t, utilsMock.HasWrittenFile(filepath.Join(ws.ReportsDirectory, "mock-project - 1-vulnerability-report.pdf")))
   143  		assert.True(t, utilsMock.HasWrittenFile(filepath.Join(ws.ReportsDirectory, "mock-project - 1-vulnerability-report.pdf")))
   144  		assert.Equal(t, 3, len(utilsMock.ExecMockRunner.Calls), "no InstallCommand must be executed")
   145  	})
   146  	t.Run("executes the InstallCommand prior to the scan", func(t *testing.T) {
   147  		ctx := context.Background()
   148  		// init
   149  		config := ScanOptions{
   150  			BuildDescriptorFile:       "my-mta.yml",
   151  			VersioningModel:           "major",
   152  			AgentDownloadURL:          "https://whitesource.com/agent.jar",
   153  			VulnerabilityReportFormat: "pdf",
   154  			Reporting:                 true,
   155  			AgentFileName:             "ua.jar",
   156  			ProductName:               "mock-product",
   157  			ProjectToken:              "mock-project-token",
   158  			InstallCommand:            "echo hello world",
   159  		}
   160  		utilsMock := newWhitesourceUtilsMock()
   161  		utilsMock.AddFile("wss-generated-file.config", []byte("key=value"))
   162  		lastUpdatedDate := time.Now().Format(ws.DateTimeLayout)
   163  		systemMock := ws.NewSystemMock(lastUpdatedDate)
   164  		systemMock.Alerts = []ws.Alert{}
   165  		scan := newWhitesourceScan(&config)
   166  		cpe := whitesourceExecuteScanCommonPipelineEnvironment{}
   167  		influx := whitesourceExecuteScanInflux{}
   168  		// test
   169  		err := runWhitesourceExecuteScan(ctx, &config, scan, utilsMock, systemMock, &cpe, &influx)
   170  		// assert
   171  		assert.NoError(t, err)
   172  		assert.Equal(t, 4, len(utilsMock.ExecMockRunner.Calls), "InstallCommand not executed")
   173  		assert.Equal(t, mock.ExecCall{Exec: "echo", Params: []string{"hello", "world"}}, utilsMock.ExecMockRunner.Calls[0], "run command/params of InstallCommand incorrect")
   174  	})
   175  	t.Run("fails if the InstallCommand fails", func(t *testing.T) {
   176  		ctx := context.Background()
   177  		// init
   178  		config := ScanOptions{
   179  			BuildDescriptorFile:       "my-mta.yml",
   180  			VersioningModel:           "major",
   181  			AgentDownloadURL:          "https://whitesource.com/agent.jar",
   182  			VulnerabilityReportFormat: "pdf",
   183  			Reporting:                 true,
   184  			AgentFileName:             "ua.jar",
   185  			ProductName:               "mock-product",
   186  			ProjectToken:              "mock-project-token",
   187  			InstallCommand:            "echo this-will-fail",
   188  		}
   189  		utilsMock := newWhitesourceUtilsMock()
   190  		utilsMock.AddFile("wss-generated-file.config", []byte("key=value"))
   191  		lastUpdatedDate := time.Now().Format(ws.DateTimeLayout)
   192  		systemMock := ws.NewSystemMock(lastUpdatedDate)
   193  		systemMock.Alerts = []ws.Alert{}
   194  		scan := newWhitesourceScan(&config)
   195  		cpe := whitesourceExecuteScanCommonPipelineEnvironment{}
   196  		influx := whitesourceExecuteScanInflux{}
   197  		utilsMock.ExecMockRunner.ShouldFailOnCommand = map[string]error{
   198  			"echo this-will-fail": errors.New("error case"),
   199  		}
   200  		// test
   201  		err := runWhitesourceExecuteScan(ctx, &config, scan, utilsMock, systemMock, &cpe, &influx)
   202  		// assert
   203  		assert.EqualError(t, err, "failed to execute WhiteSource scan: failed to execute Scan: failed to execute install command: echo this-will-fail: error case")
   204  	})
   205  }
   206  
   207  func TestCheckAndReportScanResults(t *testing.T) {
   208  	t.Parallel()
   209  	t.Run("no reports requested", func(t *testing.T) {
   210  		ctx := context.Background()
   211  		// init
   212  		config := &ScanOptions{
   213  			ProductToken: "mock-product-token",
   214  			ProjectToken: "mock-project-token",
   215  			Version:      "1",
   216  		}
   217  		scan := newWhitesourceScan(config)
   218  		utils := newWhitesourceUtilsMock()
   219  		system := ws.NewSystemMock(time.Now().Format(ws.DateTimeLayout))
   220  		influx := whitesourceExecuteScanInflux{}
   221  		// test
   222  		_, err := checkAndReportScanResults(ctx, config, scan, utils, system, &influx)
   223  		// assert
   224  		assert.NoError(t, err)
   225  		vPath := filepath.Join(ws.ReportsDirectory, "mock-project-vulnerability-report.txt")
   226  		assert.False(t, utils.HasWrittenFile(vPath))
   227  		rPath := filepath.Join(ws.ReportsDirectory, "mock-project-risk-report.pdf")
   228  		assert.False(t, utils.HasWrittenFile(rPath))
   229  	})
   230  	t.Run("check vulnerabilities - invalid limit", func(t *testing.T) {
   231  		ctx := context.Background()
   232  		// init
   233  		config := &ScanOptions{
   234  			SecurityVulnerabilities: true,
   235  			CvssSeverityLimit:       "invalid",
   236  		}
   237  		scan := newWhitesourceScan(config)
   238  		utils := newWhitesourceUtilsMock()
   239  		system := ws.NewSystemMock(time.Now().Format(ws.DateTimeLayout))
   240  		influx := whitesourceExecuteScanInflux{}
   241  		// test
   242  		_, err := checkAndReportScanResults(ctx, config, scan, utils, system, &influx)
   243  		// assert
   244  		assert.EqualError(t, err, "failed to parse parameter cvssSeverityLimit (invalid) as floating point number: strconv.ParseFloat: parsing \"invalid\": invalid syntax")
   245  	})
   246  	t.Run("check vulnerabilities - limit not hit", func(t *testing.T) {
   247  		ctx := context.Background()
   248  		// init
   249  		config := &ScanOptions{
   250  			ProductToken:            "mock-product-token",
   251  			ProjectToken:            "mock-project-token",
   252  			Version:                 "1",
   253  			SecurityVulnerabilities: true,
   254  			CvssSeverityLimit:       "6.0",
   255  		}
   256  		scan := newWhitesourceScan(config)
   257  		utils := newWhitesourceUtilsMock()
   258  		system := ws.NewSystemMock(time.Now().Format(ws.DateTimeLayout))
   259  		influx := whitesourceExecuteScanInflux{}
   260  		// test
   261  		_, err := checkAndReportScanResults(ctx, config, scan, utils, system, &influx)
   262  		// assert
   263  		assert.NoError(t, err)
   264  	})
   265  	t.Run("check vulnerabilities - limit exceeded", func(t *testing.T) {
   266  		ctx := context.Background()
   267  		// init
   268  		config := &ScanOptions{
   269  			ProductToken:                "mock-product-token",
   270  			ProjectName:                 "mock-project - 1",
   271  			ProjectToken:                "mock-project-token",
   272  			Version:                     "1",
   273  			SecurityVulnerabilities:     true,
   274  			CvssSeverityLimit:           "4",
   275  			FailOnSevereVulnerabilities: true,
   276  		}
   277  		scan := newWhitesourceScan(config)
   278  		utils := newWhitesourceUtilsMock()
   279  		system := ws.NewSystemMock(time.Now().Format(ws.DateTimeLayout))
   280  		influx := whitesourceExecuteScanInflux{}
   281  		// test
   282  		_, err := checkAndReportScanResults(ctx, config, scan, utils, system, &influx)
   283  		// assert
   284  		assert.EqualError(t, err, "1 Open Source Software Security vulnerabilities with CVSS score greater or equal to 4.0 detected in project mock-project - 1")
   285  	})
   286  }
   287  
   288  func TestResolveProjectIdentifiers(t *testing.T) {
   289  	t.Parallel()
   290  	t.Run("success", func(t *testing.T) {
   291  		// init
   292  		config := ScanOptions{
   293  			BuildTool:           "mta",
   294  			BuildDescriptorFile: "my-mta.yml",
   295  			VersioningModel:     "major",
   296  			ProductName:         "mock-product",
   297  			M2Path:              "m2/path",
   298  			ProjectSettingsFile: "project-settings.xml",
   299  			GlobalSettingsFile:  "global-settings.xml",
   300  		}
   301  		utilsMock := newWhitesourceUtilsMock()
   302  		systemMock := ws.NewSystemMock("ignored")
   303  		scan := newWhitesourceScan(&config)
   304  		// test
   305  		err := resolveProjectIdentifiers(&config, scan, utilsMock, systemMock)
   306  		// assert
   307  		if assert.NoError(t, err) {
   308  			assert.Equal(t, "mock-group-id-mock-artifact-id", scan.AggregateProjectName)
   309  			assert.Equal(t, "1", config.Version)
   310  			assert.Equal(t, "mock-product-token", config.ProductToken)
   311  			assert.Equal(t, "mta", utilsMock.usedBuildTool)
   312  			assert.Equal(t, "my-mta.yml", utilsMock.usedBuildDescriptorFile)
   313  			assert.Equal(t, "project-settings.xml", utilsMock.usedOptions.ProjectSettingsFile)
   314  			assert.Equal(t, "global-settings.xml", utilsMock.usedOptions.GlobalSettingsFile)
   315  			assert.Equal(t, "m2/path", utilsMock.usedOptions.M2Path)
   316  		}
   317  	})
   318  	t.Run("success - with version from default", func(t *testing.T) {
   319  		// init
   320  		config := ScanOptions{
   321  			BuildTool:           "mta",
   322  			BuildDescriptorFile: "my-mta.yml",
   323  			Version:             "1.2.3-20200101",
   324  			VersioningModel:     "major",
   325  			ProductName:         "mock-product",
   326  			M2Path:              "m2/path",
   327  			ProjectSettingsFile: "project-settings.xml",
   328  			GlobalSettingsFile:  "global-settings.xml",
   329  		}
   330  		utilsMock := newWhitesourceUtilsMock()
   331  		systemMock := ws.NewSystemMock("ignored")
   332  		scan := newWhitesourceScan(&config)
   333  		// test
   334  		err := resolveProjectIdentifiers(&config, scan, utilsMock, systemMock)
   335  		// assert
   336  		if assert.NoError(t, err) {
   337  			assert.Equal(t, "mock-group-id-mock-artifact-id", scan.AggregateProjectName)
   338  			assert.Equal(t, "1", config.Version)
   339  			assert.Equal(t, "mock-product-token", config.ProductToken)
   340  			assert.Equal(t, "mta", utilsMock.usedBuildTool)
   341  			assert.Equal(t, "my-mta.yml", utilsMock.usedBuildDescriptorFile)
   342  			assert.Equal(t, "project-settings.xml", utilsMock.usedOptions.ProjectSettingsFile)
   343  			assert.Equal(t, "global-settings.xml", utilsMock.usedOptions.GlobalSettingsFile)
   344  			assert.Equal(t, "m2/path", utilsMock.usedOptions.M2Path)
   345  		}
   346  	})
   347  	t.Run("success - with custom scan version", func(t *testing.T) {
   348  		// init
   349  		config := ScanOptions{
   350  			BuildTool:           "mta",
   351  			BuildDescriptorFile: "my-mta.yml",
   352  			CustomScanVersion:   "2.3.4",
   353  			VersioningModel:     "major",
   354  			ProductName:         "mock-product",
   355  			M2Path:              "m2/path",
   356  			ProjectSettingsFile: "project-settings.xml",
   357  			GlobalSettingsFile:  "global-settings.xml",
   358  		}
   359  		utilsMock := newWhitesourceUtilsMock()
   360  		systemMock := ws.NewSystemMock("ignored")
   361  		scan := newWhitesourceScan(&config)
   362  		// test
   363  		err := resolveProjectIdentifiers(&config, scan, utilsMock, systemMock)
   364  		// assert
   365  		if assert.NoError(t, err) {
   366  			assert.Equal(t, "mock-group-id-mock-artifact-id", scan.AggregateProjectName)
   367  			assert.Equal(t, "2.3.4", config.Version)
   368  			assert.Equal(t, "mock-product-token", config.ProductToken)
   369  			assert.Equal(t, "mta", utilsMock.usedBuildTool)
   370  			assert.Equal(t, "my-mta.yml", utilsMock.usedBuildDescriptorFile)
   371  			assert.Equal(t, "project-settings.xml", utilsMock.usedOptions.ProjectSettingsFile)
   372  			assert.Equal(t, "global-settings.xml", utilsMock.usedOptions.GlobalSettingsFile)
   373  			assert.Equal(t, "m2/path", utilsMock.usedOptions.M2Path)
   374  		}
   375  	})
   376  	t.Run("success - with custom scan version (projectName is filled)", func(t *testing.T) {
   377  		// init
   378  		config := ScanOptions{
   379  			BuildTool:         "mta",
   380  			CustomScanVersion: "latest",
   381  			VersioningModel:   "major",
   382  			ProductName:       "mock-product",
   383  			ProjectName:       "mock-project",
   384  			Version:           "0.0.1",
   385  		}
   386  		utilsMock := newWhitesourceUtilsMock()
   387  		systemMock := ws.NewSystemMock("ignored")
   388  		scan := newWhitesourceScan(&config)
   389  		// test
   390  		err := resolveProjectIdentifiers(&config, scan, utilsMock, systemMock)
   391  		// assert
   392  		if assert.NoError(t, err) {
   393  			assert.Equal(t, "mock-project", scan.AggregateProjectName)
   394  			assert.Equal(t, "latest", config.Version)
   395  			assert.Equal(t, "mock-product-token", config.ProductToken)
   396  		}
   397  	})
   398  	t.Run("success - with version from default (projectName is filled)", func(t *testing.T) {
   399  		// init
   400  		config := ScanOptions{
   401  			BuildTool:       "mta",
   402  			VersioningModel: "major-minor",
   403  			ProductName:     "mock-product",
   404  			ProjectName:     "mock-project",
   405  			Version:         "1.2.3",
   406  		}
   407  		utilsMock := newWhitesourceUtilsMock()
   408  		systemMock := ws.NewSystemMock("ignored")
   409  		scan := newWhitesourceScan(&config)
   410  		// test
   411  		err := resolveProjectIdentifiers(&config, scan, utilsMock, systemMock)
   412  		// assert
   413  		if assert.NoError(t, err) {
   414  			assert.Equal(t, "mock-project", scan.AggregateProjectName)
   415  			assert.Equal(t, "1.2", config.Version)
   416  			assert.Equal(t, "mock-product-token", config.ProductToken)
   417  		}
   418  	})
   419  	t.Run("retrieves token for configured project name", func(t *testing.T) {
   420  		// init
   421  		config := ScanOptions{
   422  			BuildTool:           "mta",
   423  			BuildDescriptorFile: "my-mta.yml",
   424  			VersioningModel:     "major",
   425  			ProductName:         "mock-product",
   426  			ProjectName:         "mock-project",
   427  		}
   428  		utilsMock := newWhitesourceUtilsMock()
   429  		systemMock := ws.NewSystemMock("ignored")
   430  		scan := newWhitesourceScan(&config)
   431  		// test
   432  		err := resolveProjectIdentifiers(&config, scan, utilsMock, systemMock)
   433  		// assert
   434  		if assert.NoError(t, err) {
   435  			assert.Equal(t, "mock-project", scan.AggregateProjectName)
   436  			assert.Equal(t, "1", config.Version)
   437  			assert.Equal(t, "mock-product-token", config.ProductToken)
   438  			assert.Equal(t, "mta", utilsMock.usedBuildTool)
   439  			assert.Equal(t, "my-mta.yml", utilsMock.usedBuildDescriptorFile)
   440  			assert.Equal(t, "mock-project-token", config.ProjectToken)
   441  		}
   442  	})
   443  	t.Run("product not found", func(t *testing.T) {
   444  		// init
   445  		config := ScanOptions{
   446  			BuildTool:       "mta",
   447  			VersioningModel: "major",
   448  			ProductName:     "does-not-exist",
   449  		}
   450  		utilsMock := newWhitesourceUtilsMock()
   451  		systemMock := ws.NewSystemMock("ignored")
   452  		scan := newWhitesourceScan(&config)
   453  		// test
   454  		err := resolveProjectIdentifiers(&config, scan, utilsMock, systemMock)
   455  		// assert
   456  		assert.EqualError(t, err, "error resolving product token: failed to get product by name: no product with name 'does-not-exist' found in Whitesource")
   457  	})
   458  	t.Run("product not found, created from pipeline", func(t *testing.T) {
   459  		// init
   460  		config := ScanOptions{
   461  			BuildTool:                            "mta",
   462  			CreateProductFromPipeline:            true,
   463  			EmailAddressesOfInitialProductAdmins: []string{"user1@domain.org", "user2@domain.org"},
   464  			VersioningModel:                      "major",
   465  			ProductName:                          "created-by-pipeline",
   466  		}
   467  		utilsMock := newWhitesourceUtilsMock()
   468  		systemMock := ws.NewSystemMock("ignored")
   469  		scan := newWhitesourceScan(&config)
   470  		// test
   471  		err := resolveProjectIdentifiers(&config, scan, utilsMock, systemMock)
   472  		// assert
   473  		assert.NoError(t, err)
   474  		assert.Len(t, systemMock.Products, 2)
   475  		assert.Equal(t, "created-by-pipeline", systemMock.Products[1].Name)
   476  		assert.Equal(t, "mock-product-token-1", config.ProductToken)
   477  	})
   478  }
   479  
   480  func TestCheckPolicyViolations(t *testing.T) {
   481  	t.Parallel()
   482  
   483  	t.Run("success - no violations", func(t *testing.T) {
   484  		ctx := context.Background()
   485  		config := ScanOptions{ProductName: "mock-product", Version: "1"}
   486  		scan := newWhitesourceScan(&config)
   487  		if err := scan.AppendScannedProject("testProject1"); err != nil {
   488  			t.Fail()
   489  		}
   490  		systemMock := ws.NewSystemMock("ignored")
   491  		systemMock.Alerts = []ws.Alert{}
   492  		utilsMock := newWhitesourceUtilsMock()
   493  		reportPaths := []piperutils.Path{
   494  			{Target: filepath.Join("whitesource", "report1.pdf")},
   495  			{Target: filepath.Join("whitesource", "report2.pdf")},
   496  		}
   497  		influx := whitesourceExecuteScanInflux{}
   498  
   499  		path, err := checkPolicyViolations(ctx, &config, scan, systemMock, utilsMock, reportPaths, &influx)
   500  		assert.NoError(t, err)
   501  		assert.Equal(t, filepath.Join(ws.ReportsDirectory, "whitesource-ip.json"), path.Target)
   502  
   503  		fileContent, _ := utilsMock.FileRead(path.Target)
   504  		content := string(fileContent)
   505  		assert.Contains(t, content, `"policyViolations":0`)
   506  		assert.Contains(t, content, `"reports":["report1.pdf","report2.pdf"]`)
   507  
   508  		exists, err := utilsMock.FileExists(filepath.Join(reporting.StepReportDirectory, "whitesourceExecuteScan_ip_2d3120020f3f46393a54575a7f6f5675ad536721.json"))
   509  		assert.True(t, exists)
   510  	})
   511  
   512  	t.Run("success - no reports", func(t *testing.T) {
   513  		ctx := context.Background()
   514  		config := ScanOptions{}
   515  		scan := newWhitesourceScan(&config)
   516  		if err := scan.AppendScannedProject("testProject1"); err != nil {
   517  			t.Fail()
   518  		}
   519  		systemMock := ws.NewSystemMock("ignored")
   520  		systemMock.Alerts = []ws.Alert{}
   521  		utilsMock := newWhitesourceUtilsMock()
   522  		reportPaths := []piperutils.Path{}
   523  		influx := whitesourceExecuteScanInflux{}
   524  
   525  		path, err := checkPolicyViolations(ctx, &config, scan, systemMock, utilsMock, reportPaths, &influx)
   526  		assert.NoError(t, err)
   527  
   528  		fileContent, _ := utilsMock.FileRead(path.Target)
   529  		content := string(fileContent)
   530  		assert.Contains(t, content, `reports":[]`)
   531  	})
   532  
   533  	t.Run("error - policy violations", func(t *testing.T) {
   534  		ctx := context.Background()
   535  		config := ScanOptions{FailOnSevereVulnerabilities: true}
   536  		scan := newWhitesourceScan(&config)
   537  		if err := scan.AppendScannedProject("testProject1"); err != nil {
   538  			t.Fail()
   539  		}
   540  		systemMock := ws.NewSystemMock("ignored")
   541  		systemMock.Alerts = []ws.Alert{
   542  			{Vulnerability: ws.Vulnerability{Name: "policyVul1"}},
   543  			{Vulnerability: ws.Vulnerability{Name: "policyVul2"}},
   544  		}
   545  		utilsMock := newWhitesourceUtilsMock()
   546  		reportPaths := []piperutils.Path{
   547  			{Target: "report1.pdf"},
   548  			{Target: "report2.pdf"},
   549  		}
   550  		influx := whitesourceExecuteScanInflux{}
   551  
   552  		path, err := checkPolicyViolations(ctx, &config, scan, systemMock, utilsMock, reportPaths, &influx)
   553  		assert.Contains(t, fmt.Sprint(err), "2 policy violation(s) found")
   554  
   555  		fileContent, _ := utilsMock.FileRead(path.Target)
   556  		content := string(fileContent)
   557  		assert.Contains(t, content, `"policyViolations":2`)
   558  		assert.Contains(t, content, `"reports":["report1.pdf","report2.pdf"]`)
   559  	})
   560  
   561  	t.Run("error - get alerts", func(t *testing.T) {
   562  		ctx := context.Background()
   563  		config := ScanOptions{}
   564  		scan := newWhitesourceScan(&config)
   565  		if err := scan.AppendScannedProject("testProject1"); err != nil {
   566  			t.Fail()
   567  		}
   568  		systemMock := ws.NewSystemMock("ignored")
   569  		systemMock.AlertError = fmt.Errorf("failed to read alerts")
   570  		utilsMock := newWhitesourceUtilsMock()
   571  		reportPaths := []piperutils.Path{}
   572  		influx := whitesourceExecuteScanInflux{}
   573  
   574  		_, err := checkPolicyViolations(ctx, &config, scan, systemMock, utilsMock, reportPaths, &influx)
   575  		assert.Contains(t, fmt.Sprint(err), "failed to retrieve project policy alerts from WhiteSource")
   576  	})
   577  
   578  	t.Run("error - write file", func(t *testing.T) {
   579  		ctx := context.Background()
   580  		config := ScanOptions{}
   581  		scan := newWhitesourceScan(&config)
   582  		if err := scan.AppendScannedProject("testProject1"); err != nil {
   583  			t.Fail()
   584  		}
   585  		systemMock := ws.NewSystemMock("ignored")
   586  		systemMock.Alerts = []ws.Alert{}
   587  		utilsMock := newWhitesourceUtilsMock()
   588  		utilsMock.FileWriteError = fmt.Errorf("failed to write file")
   589  		reportPaths := []piperutils.Path{}
   590  		influx := whitesourceExecuteScanInflux{}
   591  
   592  		_, err := checkPolicyViolations(ctx, &config, scan, systemMock, utilsMock, reportPaths, &influx)
   593  		assert.Contains(t, fmt.Sprint(err), "failed to write policy violation report:")
   594  	})
   595  
   596  	t.Run("failed to write json report", func(t *testing.T) {
   597  		ctx := context.Background()
   598  		config := ScanOptions{ProductName: "mock-product", Version: "1"}
   599  		scan := newWhitesourceScan(&config)
   600  		if err := scan.AppendScannedProject("testProject1"); err != nil {
   601  			t.Fail()
   602  		}
   603  		systemMock := ws.NewSystemMock("ignored")
   604  		systemMock.Alerts = []ws.Alert{}
   605  		utilsMock := newWhitesourceUtilsMock()
   606  		utilsMock.FileWriteErrors = map[string]error{
   607  			filepath.Join(reporting.StepReportDirectory, "whitesourceExecuteScan_ip_2d3120020f3f46393a54575a7f6f5675ad536721.json"): fmt.Errorf("write error"),
   608  		}
   609  		reportPaths := []piperutils.Path{}
   610  		influx := whitesourceExecuteScanInflux{}
   611  
   612  		_, err := checkPolicyViolations(ctx, &config, scan, systemMock, utilsMock, reportPaths, &influx)
   613  		assert.Contains(t, fmt.Sprint(err), "failed to write json report")
   614  	})
   615  }
   616  
   617  func TestCheckSecurityViolations(t *testing.T) {
   618  	t.Parallel()
   619  
   620  	t.Run("success - non-aggregated", func(t *testing.T) {
   621  		ctx := context.Background()
   622  		config := ScanOptions{
   623  			CvssSeverityLimit: "7",
   624  		}
   625  		scan := newWhitesourceScan(&config)
   626  		if err := scan.AppendScannedProject("testProject1"); err != nil {
   627  			t.Fail()
   628  		}
   629  		systemMock := ws.NewSystemMock("ignored")
   630  		systemMock.Alerts = []ws.Alert{
   631  			{Vulnerability: ws.Vulnerability{Name: "vul1", CVSS3Score: 6.0}},
   632  		}
   633  		utilsMock := newWhitesourceUtilsMock()
   634  		influx := whitesourceExecuteScanInflux{}
   635  
   636  		reportPaths, err := checkSecurityViolations(ctx, &config, scan, systemMock, utilsMock, &influx)
   637  		assert.NoError(t, err)
   638  		fileContent, err := utilsMock.FileRead(reportPaths[0].Target)
   639  		assert.NoError(t, err)
   640  		assert.True(t, len(fileContent) > 0)
   641  	})
   642  
   643  	t.Run("success - aggregated", func(t *testing.T) {
   644  		ctx := context.Background()
   645  		config := ScanOptions{
   646  			CvssSeverityLimit: "7",
   647  			ProjectToken:      "theProjectToken",
   648  		}
   649  		scan := newWhitesourceScan(&config)
   650  		systemMock := ws.NewSystemMock("ignored")
   651  		systemMock.Alerts = []ws.Alert{
   652  			{Vulnerability: ws.Vulnerability{Name: "vul1", CVSS3Score: 6.0}},
   653  		}
   654  		utilsMock := newWhitesourceUtilsMock()
   655  		influx := whitesourceExecuteScanInflux{}
   656  
   657  		reportPaths, err := checkSecurityViolations(ctx, &config, scan, systemMock, utilsMock, &influx)
   658  		assert.NoError(t, err)
   659  		assert.Equal(t, 3, len(reportPaths))
   660  	})
   661  
   662  	t.Run("error - wrong limit", func(t *testing.T) {
   663  		ctx := context.Background()
   664  		config := ScanOptions{CvssSeverityLimit: "x"}
   665  		scan := newWhitesourceScan(&config)
   666  		systemMock := ws.NewSystemMock("ignored")
   667  		utilsMock := newWhitesourceUtilsMock()
   668  		influx := whitesourceExecuteScanInflux{}
   669  
   670  		_, err := checkSecurityViolations(ctx, &config, scan, systemMock, utilsMock, &influx)
   671  		assert.Contains(t, fmt.Sprint(err), "failed to parse parameter cvssSeverityLimit")
   672  	})
   673  
   674  	t.Run("error - non-aggregated", func(t *testing.T) {
   675  		ctx := context.Background()
   676  		config := ScanOptions{
   677  			CvssSeverityLimit:           "5",
   678  			FailOnSevereVulnerabilities: true,
   679  		}
   680  		scan := newWhitesourceScan(&config)
   681  		if err := scan.AppendScannedProject("testProject1"); err != nil {
   682  			t.Fail()
   683  		}
   684  		systemMock := ws.NewSystemMock("ignored")
   685  		systemMock.Alerts = []ws.Alert{
   686  			{Vulnerability: ws.Vulnerability{Name: "vul1", CVSS3Score: 6.0}},
   687  		}
   688  		utilsMock := newWhitesourceUtilsMock()
   689  		influx := whitesourceExecuteScanInflux{}
   690  
   691  		reportPaths, err := checkSecurityViolations(ctx, &config, scan, systemMock, utilsMock, &influx)
   692  		assert.Contains(t, fmt.Sprint(err), "1 Open Source Software Security vulnerabilities")
   693  		fileContent, err := utilsMock.FileRead(reportPaths[0].Target)
   694  		assert.NoError(t, err)
   695  		assert.True(t, len(fileContent) > 0)
   696  	})
   697  
   698  	t.Run("error - aggregated", func(t *testing.T) {
   699  		ctx := context.Background()
   700  		config := ScanOptions{
   701  			CvssSeverityLimit:           "5",
   702  			ProjectToken:                "theProjectToken",
   703  			FailOnSevereVulnerabilities: true,
   704  		}
   705  		scan := newWhitesourceScan(&config)
   706  		systemMock := ws.NewSystemMock("ignored")
   707  		systemMock.Alerts = []ws.Alert{
   708  			{Vulnerability: ws.Vulnerability{Name: "vul1", CVSS3Score: 6.0}},
   709  		}
   710  		utilsMock := newWhitesourceUtilsMock()
   711  		influx := whitesourceExecuteScanInflux{}
   712  
   713  		reportPaths, err := checkSecurityViolations(ctx, &config, scan, systemMock, utilsMock, &influx)
   714  		assert.Contains(t, fmt.Sprint(err), "1 Open Source Software Security vulnerabilities")
   715  		assert.Equal(t, 3, len(reportPaths))
   716  	})
   717  }
   718  
   719  func TestCheckProjectSecurityViolations(t *testing.T) {
   720  	project := ws.Project{Name: "testProject - 1", Token: "testToken"}
   721  
   722  	t.Run("success - no alerts", func(t *testing.T) {
   723  		systemMock := ws.NewSystemMock("ignored")
   724  		systemMock.Alerts = []ws.Alert{}
   725  		influx := whitesourceExecuteScanInflux{}
   726  
   727  		severeVulnerabilities, alerts, assessedAlerts, err := checkProjectSecurityViolations(&ScanOptions{FailOnSevereVulnerabilities: true}, 7.0, project, systemMock, &[]format.Assessment{}, &influx)
   728  		assert.NoError(t, err)
   729  		assert.Equal(t, 0, severeVulnerabilities)
   730  		assert.Equal(t, 0, len(alerts))
   731  		assert.Equal(t, 0, len(assessedAlerts))
   732  	})
   733  
   734  	t.Run("error - some vulnerabilities", func(t *testing.T) {
   735  		systemMock := ws.NewSystemMock("ignored")
   736  		systemMock.Alerts = []ws.Alert{
   737  			{Vulnerability: ws.Vulnerability{CVSS3Score: 7, Name: "CVE-2025-001"}, Library: ws.Library{KeyID: 42, Name: "test", GroupID: "com.sap", ArtifactID: "test", Version: "1.2.3", LibType: "Java"}},
   738  			{Vulnerability: ws.Vulnerability{CVSS3Score: 6, Name: "CVE-2025-002"}, Library: ws.Library{KeyID: 42, Name: "test", GroupID: "com.sap", ArtifactID: "test", Version: "1.2.3", LibType: "Java"}},
   739  		}
   740  		influx := whitesourceExecuteScanInflux{}
   741  
   742  		severeVulnerabilities, alerts, assessedAlerts, err := checkProjectSecurityViolations(&ScanOptions{FailOnSevereVulnerabilities: true}, 7.0, project, systemMock, &[]format.Assessment{}, &influx)
   743  		assert.Contains(t, fmt.Sprint(err), "1 Open Source Software Security vulnerabilities")
   744  		assert.Equal(t, 1, severeVulnerabilities)
   745  		assert.Equal(t, 2, len(alerts))
   746  		assert.Equal(t, 0, len(assessedAlerts))
   747  	})
   748  
   749  	t.Run("success - assessed vulnerabilities", func(t *testing.T) {
   750  		systemMock := ws.NewSystemMock("ignored")
   751  		systemMock.Alerts = []ws.Alert{
   752  			{Vulnerability: ws.Vulnerability{CVSS3Score: 7.8, Name: "CVE-2025-001"}, Library: ws.Library{KeyID: 42, Name: "test", GroupID: "com.sap", ArtifactID: "test", Version: "1.2.3", LibType: "Java"}},
   753  			{Vulnerability: ws.Vulnerability{CVSS3Score: 6, Name: "CVE-2025-002"}, Library: ws.Library{KeyID: 42, Name: "test", GroupID: "com.sap", ArtifactID: "test", Version: "1.2.3", LibType: "Java"}},
   754  		}
   755  		influx := whitesourceExecuteScanInflux{}
   756  
   757  		severeVulnerabilities, alerts, assessedAlerts, err := checkProjectSecurityViolations(&ScanOptions{FailOnSevereVulnerabilities: true}, 7.0, project, systemMock, &[]format.Assessment{{Vulnerability: "CVE-2025-001", Purls: []format.Purl{{Purl: "pkg:/maven/com.sap/test@1.2.3"}}}, {Vulnerability: "CVE-2025-002", Purls: []format.Purl{{Purl: "pkg:/maven/com.sap/test@1.2.3"}}}}, &influx)
   758  		assert.NoError(t, err)
   759  		assert.Equal(t, 0, severeVulnerabilities)
   760  		assert.Equal(t, 0, len(alerts))
   761  		assert.Equal(t, 2, len(assessedAlerts))
   762  	})
   763  
   764  	t.Run("error - WhiteSource failure", func(t *testing.T) {
   765  		systemMock := ws.NewSystemMock("ignored")
   766  		systemMock.AlertError = fmt.Errorf("failed to read alerts")
   767  		influx := whitesourceExecuteScanInflux{}
   768  
   769  		_, _, _, err := checkProjectSecurityViolations(&ScanOptions{FailOnSevereVulnerabilities: true}, 7.0, project, systemMock, &[]format.Assessment{}, &influx)
   770  		assert.Contains(t, fmt.Sprint(err), "failed to retrieve project alerts from WhiteSource")
   771  	})
   772  }
   773  
   774  func TestAggregateVersionWideLibraries(t *testing.T) {
   775  	t.Parallel()
   776  	t.Run("happy path", func(t *testing.T) {
   777  		// init
   778  		config := &ScanOptions{
   779  			FailOnSevereVulnerabilities: true,
   780  			ProductToken:                "mock-product-token",
   781  			Version:                     "1",
   782  		}
   783  		utils := newWhitesourceUtilsMock()
   784  		system := ws.NewSystemMock("2010-05-30 00:15:00 +0100")
   785  		// test
   786  		err := aggregateVersionWideLibraries(config, utils, system)
   787  		// assert
   788  		resource := filepath.Join(ws.ReportsDirectory, "libraries-20100510-001542.csv")
   789  		if assert.NoError(t, err) && assert.True(t, utils.HasWrittenFile(resource)) {
   790  			contents, _ := utils.FileRead(resource)
   791  			asString := string(contents)
   792  			assert.Equal(t, "Library Name, Project Name\nmock-library, mock-project\n", asString)
   793  			c, _ := utils.ReadFile("/whitesourceExecuteScan_reports.json")
   794  			assert.NotEmpty(t, c)
   795  		}
   796  	})
   797  }
   798  
   799  func TestAggregateVersionWideVulnerabilities(t *testing.T) {
   800  	t.Parallel()
   801  	t.Run("happy path", func(t *testing.T) {
   802  		// init
   803  		config := &ScanOptions{
   804  			ProductToken: "mock-product-token",
   805  			Version:      "1",
   806  		}
   807  		utils := newWhitesourceUtilsMock()
   808  		system := ws.NewSystemMock("2010-05-30 00:15:00 +0100")
   809  		// test
   810  		err := aggregateVersionWideVulnerabilities(config, utils, system)
   811  		// assert
   812  		resource := filepath.Join(ws.ReportsDirectory, "project-names-aggregated.txt")
   813  		assert.NoError(t, err)
   814  		if assert.True(t, utils.HasWrittenFile(resource)) {
   815  			contents, _ := utils.FileRead(resource)
   816  			asString := string(contents)
   817  			assert.Equal(t, "mock-project - 1\n", asString)
   818  		}
   819  		reportSheet := filepath.Join(ws.ReportsDirectory, "vulnerabilities-20100510-001542.xlsx")
   820  		sheetContents, err := utils.FileRead(reportSheet)
   821  		assert.NoError(t, err)
   822  		assert.NotEmpty(t, sheetContents)
   823  		c, _ := utils.ReadFile("whitesourceExecuteScan_reports.json")
   824  		assert.NotEmpty(t, c)
   825  	})
   826  }
   827  
   828  func TestPersistScannedProjects(t *testing.T) {
   829  	t.Parallel()
   830  	t.Run("write 1 scanned projects", func(t *testing.T) {
   831  		// init
   832  		cpe := whitesourceExecuteScanCommonPipelineEnvironment{}
   833  		config := &ScanOptions{Version: "1"}
   834  		scan := newWhitesourceScan(config)
   835  		_ = scan.AppendScannedProject("project")
   836  		// test
   837  		persistScannedProjects(config, scan, &cpe)
   838  		// assert
   839  		assert.Equal(t, []string{"project - 1"}, cpe.custom.whitesourceProjectNames)
   840  	})
   841  	t.Run("write 2 scanned projects", func(t *testing.T) {
   842  		// init
   843  		cpe := whitesourceExecuteScanCommonPipelineEnvironment{}
   844  		config := &ScanOptions{Version: "1"}
   845  		scan := newWhitesourceScan(config)
   846  		_ = scan.AppendScannedProject("project-app")
   847  		_ = scan.AppendScannedProject("project-db")
   848  		// test
   849  		persistScannedProjects(config, scan, &cpe)
   850  		// assert
   851  		assert.Equal(t, []string{"project-app - 1", "project-db - 1"}, cpe.custom.whitesourceProjectNames)
   852  	})
   853  	t.Run("write no projects", func(t *testing.T) {
   854  		// init
   855  		cpe := whitesourceExecuteScanCommonPipelineEnvironment{}
   856  		config := &ScanOptions{Version: "1"}
   857  		scan := newWhitesourceScan(config)
   858  		// test
   859  		persistScannedProjects(config, scan, &cpe)
   860  		// assert
   861  		assert.Equal(t, []string{}, cpe.custom.whitesourceProjectNames)
   862  	})
   863  	t.Run("write aggregated project", func(t *testing.T) {
   864  		// init
   865  		cpe := whitesourceExecuteScanCommonPipelineEnvironment{}
   866  		config := &ScanOptions{ProjectName: "project", Version: "1"}
   867  		scan := newWhitesourceScan(config)
   868  		// test
   869  		persistScannedProjects(config, scan, &cpe)
   870  		// assert
   871  		assert.Equal(t, []string{"project - 1"}, cpe.custom.whitesourceProjectNames)
   872  	})
   873  }