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 }