github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/pkg/whitesource/reporting_test.go (about)

     1  //go:build unit
     2  // +build unit
     3  
     4  package whitesource
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"os"
    10  	"path/filepath"
    11  	"testing"
    12  
    13  	cdx "github.com/CycloneDX/cyclonedx-go"
    14  
    15  	"github.com/SAP/jenkins-library/pkg/format"
    16  	"github.com/SAP/jenkins-library/pkg/mock"
    17  	"github.com/SAP/jenkins-library/pkg/piperutils"
    18  	"github.com/SAP/jenkins-library/pkg/reporting"
    19  	"github.com/SAP/jenkins-library/pkg/versioning"
    20  	"github.com/stretchr/testify/assert"
    21  )
    22  
    23  func TestCreateCustomVulnerabilityReport(t *testing.T) {
    24  	t.Parallel()
    25  
    26  	t.Run("success case", func(t *testing.T) {
    27  		config := &ScanOptions{}
    28  		scan := &Scan{
    29  			AggregateProjectName: config.ProjectName,
    30  			ProductVersion:       config.ProductVersion,
    31  		}
    32  		scan.AppendScannedProject("testProject")
    33  		alerts := []Alert{
    34  			{Library: Library{Filename: "vul1"}, Vulnerability: Vulnerability{CVSS3Score: 7.0, Score: 6}},
    35  			{Library: Library{Filename: "vul2"}, Vulnerability: Vulnerability{CVSS3Score: 8.0, TopFix: Fix{Message: "this is the top fix"}}},
    36  			{Library: Library{Filename: "vul3"}, Vulnerability: Vulnerability{Score: 6}},
    37  		}
    38  
    39  		scanReport := CreateCustomVulnerabilityReport(config.ProductName, scan, &alerts, 7.0)
    40  
    41  		assert.Equal(t, "WhiteSource Security Vulnerability Report", scanReport.Title())
    42  		assert.Equal(t, 3, len(scanReport.DetailTable.Rows))
    43  
    44  		// assert that library info is filled and sorting has been executed
    45  		assert.Equal(t, "vul2", scanReport.DetailTable.Rows[0].Columns[5].Content)
    46  		assert.Equal(t, "vul1", scanReport.DetailTable.Rows[1].Columns[5].Content)
    47  		assert.Equal(t, "vul3", scanReport.DetailTable.Rows[2].Columns[5].Content)
    48  
    49  		// assert that CVSS version identification has been done
    50  		assert.Equal(t, "v3", scanReport.DetailTable.Rows[0].Columns[3].Content)
    51  		assert.Equal(t, "v3", scanReport.DetailTable.Rows[1].Columns[3].Content)
    52  		assert.Equal(t, "v2", scanReport.DetailTable.Rows[2].Columns[3].Content)
    53  
    54  		// assert proper rating and styling of high prio issues
    55  		assert.Equal(t, "8", scanReport.DetailTable.Rows[0].Columns[2].Content)
    56  		assert.Equal(t, "7", scanReport.DetailTable.Rows[1].Columns[2].Content)
    57  		assert.Equal(t, "6", scanReport.DetailTable.Rows[2].Columns[2].Content)
    58  		assert.Equal(t, "red-cell", scanReport.DetailTable.Rows[0].Columns[2].Style.String())
    59  		assert.Equal(t, "red-cell", scanReport.DetailTable.Rows[1].Columns[2].Style.String())
    60  		assert.Equal(t, "yellow-cell", scanReport.DetailTable.Rows[2].Columns[2].Style.String())
    61  
    62  		assert.Contains(t, scanReport.DetailTable.Rows[0].Columns[10].Content, "this is the top fix")
    63  
    64  	})
    65  }
    66  
    67  func TestCreateCycloneSBOM(t *testing.T) {
    68  	t.Parallel()
    69  
    70  	t.Run("success case", func(t *testing.T) {
    71  		config := &ScanOptions{}
    72  		scan := &Scan{
    73  			AgentName:            "Mend Unified Agent",
    74  			AgentVersion:         "3.3.3",
    75  			AggregateProjectName: config.ProjectName,
    76  			BuildTool:            "maven",
    77  			ProductVersion:       config.ProductVersion,
    78  			Coordinates:          versioning.Coordinates{GroupID: "com.sap", ArtifactID: "myproduct", Version: "1.3.4"},
    79  		}
    80  		scan.AppendScannedProject("testProject")
    81  		alerts := []Alert{
    82  			{Library: Library{KeyID: 42, Name: "log4j", GroupID: "apache-logging", ArtifactID: "log4j", Filename: "vul1"}, Vulnerability: Vulnerability{CVSS3Score: 7.0, Score: 6}},
    83  			{Library: Library{KeyID: 43, Name: "commons-lang", GroupID: "apache-commons", ArtifactID: "commons-lang", Filename: "vul2"}, Vulnerability: Vulnerability{CVSS3Score: 8.0, TopFix: Fix{Message: "this is the top fix"}}},
    84  			{Library: Library{KeyID: 42, Name: "log4j", GroupID: "apache-logging", ArtifactID: "log4j", Filename: "vul3"}, Vulnerability: Vulnerability{Score: 6}},
    85  		}
    86  
    87  		assessedAlerts := []Alert{
    88  			{Library: Library{KeyID: 42, Name: "log4j", GroupID: "apache-logging", ArtifactID: "log4j", Filename: "vul4"}, Vulnerability: Vulnerability{Name: "CVE-23456", CVSS3Score: 7.0, Score: 6}, Assessment: &format.Assessment{Vulnerability: "CVE-23456", Status: format.Relevant, Analysis: format.Mitigated}},
    89  		}
    90  
    91  		libraries := []Library{
    92  			{KeyID: 42, Name: "log4j", GroupID: "apache-logging", ArtifactID: "log4j", Filename: "vul1", Dependencies: []Library{{KeyID: 43, Name: "commons-lang", GroupID: "apache-commons", ArtifactID: "commons-lang", Filename: "vul2"}}},
    93  			{KeyID: 42, Name: "log4j", GroupID: "apache-logging", ArtifactID: "log4j", Filename: "vul3"},
    94  		}
    95  
    96  		contents, err := CreateCycloneSBOM(scan, &libraries, &alerts, &assessedAlerts)
    97  		assert.NoError(t, err, "unexpected error")
    98  		buffer := bytes.NewBuffer(contents)
    99  		decoder := cdx.NewBOMDecoder(buffer, cdx.BOMFileFormatXML)
   100  		bom := cdx.NewBOM()
   101  		decoder.Decode(bom)
   102  
   103  		assert.NotNil(t, bom, "BOM was nil")
   104  		assert.NotEmpty(t, bom.SpecVersion)
   105  
   106  		components := *bom.Components
   107  		vulnerabilities := *bom.Vulnerabilities
   108  		assert.Equal(t, 2, len(components))
   109  		assert.Equal(t, true, components[0].Name == "log4j" || components[0].Name == "commons-lang")
   110  		assert.Equal(t, true, components[1].Name == "log4j" || components[1].Name == "commons-lang")
   111  		assert.Equal(t, true, components[0].Name != components[1].Name)
   112  		assert.Equal(t, 4, len(vulnerabilities))
   113  		assert.NotNil(t, vulnerabilities[3].Analysis)
   114  		assert.Equal(t, cdx.IAJProtectedByMitigatingControl, vulnerabilities[3].Analysis.Justification)
   115  	})
   116  
   117  	t.Run("success - golden", func(t *testing.T) {
   118  		config := &ScanOptions{ProjectName: "myproduct - 1.3.4", ProductVersion: "1"}
   119  		scan := &Scan{
   120  			AgentName:            "Mend Unified Agent",
   121  			AgentVersion:         "3.3.3",
   122  			scannedProjects:      map[string]Project{"testProject": {Name: "testProject", Token: "projectToken-567"}},
   123  			AggregateProjectName: config.ProjectName,
   124  			BuildTool:            "maven",
   125  			ProductVersion:       config.ProductVersion,
   126  			ProductToken:         "productToken-123",
   127  			Coordinates:          versioning.Coordinates{GroupID: "com.sap", ArtifactID: "myproduct", Version: "1.3.4"},
   128  		}
   129  		scan.AppendScannedProject("testProject")
   130  
   131  		lib3 := Library{KeyID: 43, Name: "commons-lang", GroupID: "apache-commons", ArtifactID: "commons-lang", Version: "2.4.30", LibType: "Java", Filename: "vul2"}
   132  		lib4 := Library{KeyID: 45, Name: "commons-lang", GroupID: "apache-commons", ArtifactID: "commons-lang", Version: "3.15", LibType: "Java", Filename: "novul"}
   133  		lib1 := Library{KeyID: 42, Name: "log4j", GroupID: "apache-logging", ArtifactID: "log4j", Version: "1.14", LibType: "Java", Filename: "vul1", Dependencies: []Library{lib3}}
   134  		lib2 := Library{KeyID: 44, Name: "log4j", GroupID: "apache-logging", ArtifactID: "log4j", Version: "3.25", LibType: "Java", Filename: "vul3", Dependencies: []Library{lib4}}
   135  
   136  		alerts := []Alert{
   137  			{Library: lib1, Vulnerability: Vulnerability{Name: "CVE-2022-001", CVSS3Score: 7, Score: 6, CVSS3Severity: "high", Severity: "medium", PublishDate: "01.01.2022"}},
   138  			{Library: lib3, Vulnerability: Vulnerability{Name: "CVE-2022-002", CVSS3Score: 8, CVSS3Severity: "high", PublishDate: "02.01.2022", TopFix: Fix{Message: "this is the top fix"}}},
   139  			{Library: lib2, Vulnerability: Vulnerability{Name: "CVE-2022-003", Score: 6, Severity: "medium", PublishDate: "03.01.2022"}},
   140  		}
   141  
   142  		assessedAlerts := []Alert{}
   143  
   144  		libraries := []Library{
   145  			lib1,
   146  			lib2,
   147  		}
   148  
   149  		contents, err := CreateCycloneSBOM(scan, &libraries, &alerts, &assessedAlerts)
   150  		assert.NoError(t, err, "unexpected error")
   151  
   152  		goldenFilePath := filepath.Join("testdata", "sbom.golden")
   153  		expected, err := os.ReadFile(goldenFilePath)
   154  		assert.NoError(t, err)
   155  
   156  		assert.Equal(t, string(expected), string(contents))
   157  	})
   158  }
   159  
   160  func TestWriteCycloneSBOM(t *testing.T) {
   161  	t.Parallel()
   162  
   163  	var utilsMock piperutils.FileUtils
   164  	utilsMock = &mock.FilesMock{}
   165  
   166  	t.Run("success case", func(t *testing.T) {
   167  		paths, err := WriteCycloneSBOM([]byte{1, 2, 3, 4}, utilsMock)
   168  		assert.NoError(t, err, "unexpexted error")
   169  		assert.Equal(t, 1, len(paths))
   170  		assert.Equal(t, "whitesource/piper_whitesource_sbom.xml", paths[0].Target)
   171  
   172  		exists, err := utilsMock.FileExists(paths[0].Target)
   173  		assert.NoError(t, err)
   174  		assert.True(t, exists)
   175  	})
   176  }
   177  
   178  func TestCreateSarifResultFile(t *testing.T) {
   179  	scan := &Scan{ProductVersion: "1"}
   180  	scan.AppendScannedProject("project1")
   181  	scan.AgentName = "Some test agent"
   182  	scan.AgentVersion = "1.2.6"
   183  	alerts := []Alert{
   184  		{Library: Library{Filename: "vul1", ArtifactID: "org.some.lib"}, Vulnerability: Vulnerability{Name: "CVE-2022-001", CVSS3Score: 7.0, Score: 6}},
   185  		{Library: Library{Filename: "vul2", ArtifactID: "org.some.lib"}, Vulnerability: Vulnerability{Name: "CVE-2022-002", CVSS3Score: 8.0, TopFix: Fix{Message: "this is the top fix"}}},
   186  		{Library: Library{Filename: "vul3", ArtifactID: "org.some.lib2"}, Vulnerability: Vulnerability{Name: "CVE-2022-003", Score: 6}},
   187  	}
   188  
   189  	sarif := CreateSarifResultFile(scan, &alerts)
   190  
   191  	assert.Equal(t, "https://docs.oasis-open.org/sarif/sarif/v2.1.0/cos02/schemas/sarif-schema-2.1.0.json", sarif.Schema)
   192  	assert.Equal(t, "2.1.0", sarif.Version)
   193  	assert.Equal(t, 1, len(sarif.Runs))
   194  	assert.Equal(t, "Some test agent", sarif.Runs[0].Tool.Driver.Name)
   195  	assert.Equal(t, "1.2.6", sarif.Runs[0].Tool.Driver.Version)
   196  	assert.Equal(t, 3, len(sarif.Runs[0].Tool.Driver.Rules))
   197  	assert.Equal(t, 3, len(sarif.Runs[0].Results))
   198  	// TODO add more extensive verification once we agree on the format details
   199  }
   200  
   201  func TestWriteCustomVulnerabilityReports(t *testing.T) {
   202  
   203  	t.Run("success", func(t *testing.T) {
   204  		productName := "mock-product"
   205  		scan := &Scan{ProductVersion: "1"}
   206  		scan.AppendScannedProject("project1")
   207  		scan.AppendScannedProject("project2")
   208  
   209  		scanReport := reporting.ScanReport{}
   210  		var utilsMock piperutils.FileUtils
   211  		utilsMock = &mock.FilesMock{}
   212  
   213  		reportPaths, err := WriteCustomVulnerabilityReports(productName, scan, scanReport, utilsMock)
   214  
   215  		assert.NoError(t, err)
   216  		assert.Equal(t, 1, len(reportPaths))
   217  
   218  		exists, err := utilsMock.FileExists(reportPaths[0].Target)
   219  		assert.NoError(t, err)
   220  		assert.True(t, exists)
   221  
   222  		exists, err = utilsMock.FileExists(filepath.Join(reporting.StepReportDirectory, "whitesourceExecuteScan_oss_27322f16a39c10c852ba6639538140a03e08e93f.json"))
   223  		assert.NoError(t, err)
   224  		assert.True(t, exists)
   225  	})
   226  
   227  	t.Run("failed to write HTML report", func(t *testing.T) {
   228  		productName := "mock-product"
   229  		scan := &Scan{ProductVersion: "1"}
   230  		scanReport := reporting.ScanReport{}
   231  		utilsMock := &mock.FilesMock{}
   232  		utilsMock.FileWriteErrors = map[string]error{
   233  			filepath.Join(ReportsDirectory, "piper_whitesource_vulnerability_report.html"): fmt.Errorf("write error"),
   234  		}
   235  
   236  		_, err := WriteCustomVulnerabilityReports(productName, scan, scanReport, utilsMock)
   237  		assert.Contains(t, fmt.Sprint(err), "failed to write html report")
   238  	})
   239  
   240  	t.Run("failed to write json report", func(t *testing.T) {
   241  		productName := "mock-product"
   242  		scan := &Scan{ProductVersion: "1"}
   243  		scan.AppendScannedProject("project1")
   244  		scanReport := reporting.ScanReport{}
   245  		utilsMock := &mock.FilesMock{}
   246  		utilsMock.FileWriteErrors = map[string]error{
   247  			filepath.Join(reporting.StepReportDirectory, "whitesourceExecuteScan_oss_e860d3a7cc8ca3261f065773404ba43e9a0b9d5b.json"): fmt.Errorf("write error"),
   248  		}
   249  
   250  		_, err := WriteCustomVulnerabilityReports(productName, scan, scanReport, utilsMock)
   251  		assert.Contains(t, fmt.Sprint(err), "failed to write json report")
   252  	})
   253  }
   254  
   255  func TestWriteSarifFile(t *testing.T) {
   256  
   257  	t.Run("success", func(t *testing.T) {
   258  		sarif := format.SARIF{}
   259  		var utilsMock piperutils.FileUtils
   260  		utilsMock = &mock.FilesMock{}
   261  
   262  		reportPaths, err := WriteSarifFile(&sarif, utilsMock)
   263  
   264  		assert.NoError(t, err)
   265  		assert.Equal(t, 1, len(reportPaths))
   266  
   267  		exists, err := utilsMock.FileExists(reportPaths[0].Target)
   268  		assert.NoError(t, err)
   269  		assert.True(t, exists)
   270  
   271  		exists, err = utilsMock.FileExists(filepath.Join(ReportsDirectory, "piper_whitesource_vulnerability.sarif"))
   272  		assert.NoError(t, err)
   273  		assert.True(t, exists)
   274  	})
   275  
   276  	t.Run("failed to write HTML report", func(t *testing.T) {
   277  		sarif := format.SARIF{}
   278  		utilsMock := &mock.FilesMock{}
   279  		utilsMock.FileWriteErrors = map[string]error{
   280  			filepath.Join(ReportsDirectory, "piper_whitesource_vulnerability.sarif"): fmt.Errorf("write error"),
   281  		}
   282  
   283  		_, err := WriteSarifFile(&sarif, utilsMock)
   284  		assert.Contains(t, fmt.Sprint(err), "failed to write SARIF file")
   285  	})
   286  }
   287  
   288  func TestCountSecurityVulnerabilities(t *testing.T) {
   289  
   290  	alerts := []Alert{
   291  		{Vulnerability: Vulnerability{CVSS3Score: 7.1}},
   292  		{Vulnerability: Vulnerability{CVSS3Score: 7}},
   293  		{Vulnerability: Vulnerability{CVSS3Score: 6}},
   294  	}
   295  
   296  	severe, nonSevere := CountSecurityVulnerabilities(&alerts, 7.0)
   297  	assert.Equal(t, 2, severe)
   298  	assert.Equal(t, 1, nonSevere)
   299  }
   300  
   301  func TestIsSevereVulnerability(t *testing.T) {
   302  	tt := []struct {
   303  		alert    Alert
   304  		limit    float64
   305  		expected bool
   306  	}{
   307  		{alert: Alert{Vulnerability: Vulnerability{CVSS3Score: 0}}, limit: 0, expected: true},
   308  		{alert: Alert{Vulnerability: Vulnerability{CVSS3Score: 6.9, Score: 6}}, limit: 7.0, expected: false},
   309  		{alert: Alert{Vulnerability: Vulnerability{CVSS3Score: 7.0, Score: 6}}, limit: 7.0, expected: true},
   310  		{alert: Alert{Vulnerability: Vulnerability{CVSS3Score: 7.1, Score: 6}}, limit: 7.0, expected: true},
   311  		{alert: Alert{Vulnerability: Vulnerability{CVSS3Score: 6, Score: 6.9}}, limit: 7.0, expected: false},
   312  		{alert: Alert{Vulnerability: Vulnerability{CVSS3Score: 6, Score: 7.0}}, limit: 7.0, expected: false},
   313  		{alert: Alert{Vulnerability: Vulnerability{CVSS3Score: 6, Score: 7.1}}, limit: 7.0, expected: false},
   314  		{alert: Alert{Vulnerability: Vulnerability{Score: 6.9}}, limit: 7.0, expected: false},
   315  		{alert: Alert{Vulnerability: Vulnerability{Score: 7.0}}, limit: 7.0, expected: true},
   316  		{alert: Alert{Vulnerability: Vulnerability{Score: 7.1}}, limit: 7.0, expected: true},
   317  	}
   318  
   319  	for i, test := range tt {
   320  		assert.Equalf(t, test.expected, isSevereVulnerability(test.alert, test.limit), "run %v failed", i)
   321  	}
   322  }
   323  
   324  func TestVulnerabilityScore(t *testing.T) {
   325  	t.Parallel()
   326  
   327  	tt := []struct {
   328  		alert    Alert
   329  		expected float64
   330  	}{
   331  		{alert: Alert{Vulnerability: Vulnerability{CVSS3Score: 7.0, Score: 6}}, expected: 7.0},
   332  		{alert: Alert{Vulnerability: Vulnerability{CVSS3Score: 7.0}}, expected: 7.0},
   333  		{alert: Alert{Vulnerability: Vulnerability{Score: 6}}, expected: 6},
   334  	}
   335  	for i, test := range tt {
   336  		assert.Equalf(t, test.expected, vulnerabilityScore(test.alert), "run %v failed", i)
   337  	}
   338  }
   339  
   340  func TestGetAuditInformation(t *testing.T) {
   341  	tt := []struct {
   342  		name     string
   343  		alert    Alert
   344  		expected *format.SarifProperties
   345  	}{
   346  		{
   347  			name: "New not audited alert",
   348  			alert: Alert{
   349  				Status: "OPEN",
   350  			},
   351  			expected: &format.SarifProperties{
   352  				Audited:               false,
   353  				ToolAuditMessage:      "",
   354  				UnifiedAuditState:     "new",
   355  				AuditRequirement:      format.AUDIT_REQUIREMENT_GROUP_1_DESC,
   356  				AuditRequirementIndex: format.AUDIT_REQUIREMENT_GROUP_1_INDEX,
   357  			},
   358  		},
   359  		{
   360  			name: "Audited alert",
   361  			alert: Alert{
   362  				Status:   "IGNORE",
   363  				Comments: "Not relevant alert",
   364  				Vulnerability: Vulnerability{
   365  					CVSS3Score:    9.3,
   366  					CVSS3Severity: "critical",
   367  				},
   368  			},
   369  			expected: &format.SarifProperties{
   370  				Audited:               true,
   371  				ToolAuditMessage:      "Not relevant alert",
   372  				UnifiedAuditState:     "notRelevant",
   373  				UnifiedSeverity:       "critical",
   374  				UnifiedCriticality:    9.3,
   375  				AuditRequirement:      format.AUDIT_REQUIREMENT_GROUP_1_DESC,
   376  				AuditRequirementIndex: format.AUDIT_REQUIREMENT_GROUP_1_INDEX,
   377  			},
   378  		},
   379  		{
   380  			name: "Alert with incorrect status",
   381  			alert: Alert{
   382  				Status:   "Not correct",
   383  				Comments: "Some comment",
   384  			},
   385  			expected: &format.SarifProperties{
   386  				Audited:               false,
   387  				ToolAuditMessage:      "",
   388  				UnifiedAuditState:     "new",
   389  				AuditRequirement:      format.AUDIT_REQUIREMENT_GROUP_1_DESC,
   390  				AuditRequirementIndex: format.AUDIT_REQUIREMENT_GROUP_1_INDEX,
   391  			},
   392  		},
   393  		{
   394  			name: "Not audited alert",
   395  			alert: Alert{
   396  				Assessment: &format.Assessment{
   397  					Status:   format.NotRelevant,
   398  					Analysis: format.FixedByDevTeam,
   399  				},
   400  				Status:   "OPEN",
   401  				Comments: "New alert",
   402  			},
   403  			expected: &format.SarifProperties{
   404  				Audited:               true,
   405  				ToolAuditMessage:      string(format.FixedByDevTeam),
   406  				UnifiedAuditState:     "notRelevant",
   407  				AuditRequirement:      format.AUDIT_REQUIREMENT_GROUP_1_DESC,
   408  				AuditRequirementIndex: format.AUDIT_REQUIREMENT_GROUP_1_INDEX,
   409  			},
   410  		},
   411  	}
   412  
   413  	for _, test := range tt {
   414  		t.Run(test.name, func(t *testing.T) {
   415  			assert.Equal(t, test.expected, getAuditInformation(test.alert))
   416  		})
   417  	}
   418  }