diff --git a/audit_test.go b/audit_test.go index 2346c6b1..0e270d6d 100644 --- a/audit_test.go +++ b/audit_test.go @@ -173,14 +173,14 @@ func TestXrayAuditNugetJson(t *testing.T) { projectName: "multi", format: string(format.Json), restoreTech: "dotnet", - minVulnerabilities: 5, + minVulnerabilities: 4, minLicences: 3, }, { projectName: "multi", format: string(format.Json), restoreTech: "", - minVulnerabilities: 5, + minVulnerabilities: 4, minLicences: 3, }, } @@ -329,7 +329,7 @@ func TestXrayAuditMultiProjects(t *testing.T) { defer securityTestUtils.CleanTestsHomeEnv() output := securityTests.PlatformCli.WithoutCredentials().RunCliCmdWithOutput(t, "audit", "--format="+string(format.SimpleJson), workingDirsFlag) securityTestUtils.VerifySimpleJsonScanResults(t, output, 35, 0) - securityTestUtils.VerifySimpleJsonJasResults(t, output, 1, 9, 7, 3) + securityTestUtils.VerifySimpleJsonJasResults(t, output, 1, 9, 0, 0, 0, 25, 1) } func TestXrayAuditPipJson(t *testing.T) { @@ -433,18 +433,18 @@ func addDummyPackageDescriptor(t *testing.T, hasPackageJson bool) { func TestXrayAuditJasSimpleJson(t *testing.T) { output := testXrayAuditJas(t, string(format.SimpleJson), filepath.Join("jas", "jas-test")) - securityTestUtils.VerifySimpleJsonJasResults(t, output, 1, 9, 7, 2) + securityTestUtils.VerifySimpleJsonJasResults(t, output, 1, 9, 7, 3, 0, 2, 2) } func TestXrayAuditJasSimpleJsonWithConfig(t *testing.T) { output := testXrayAuditJas(t, string(format.SimpleJson), filepath.Join("jas", "jas-config")) - securityTestUtils.VerifySimpleJsonJasResults(t, output, 0, 0, 1, 2) + securityTestUtils.VerifySimpleJsonJasResults(t, output, 0, 0, 1, 3, 0, 2, 2) } func TestXrayAuditJasNoViolationsSimpleJson(t *testing.T) { output := testXrayAuditJas(t, string(format.SimpleJson), filepath.Join("package-managers", "npm", "npm")) securityTestUtils.VerifySimpleJsonScanResults(t, output, 1, 0) - securityTestUtils.VerifySimpleJsonJasResults(t, output, 0, 0, 0, 0) + securityTestUtils.VerifySimpleJsonJasResults(t, output, 0, 0, 0, 0, 0, 0, 1) } func testXrayAuditJas(t *testing.T, format string, project string) string { @@ -509,7 +509,7 @@ func TestXrayRecursiveScan(t *testing.T) { output := securityTests.PlatformCli.RunCliCmdWithOutput(t, "audit", "--format=json") // We anticipate the identification of five vulnerabilities: four originating from the .NET project and one from the NPM project. - securityTestUtils.VerifyJsonScanResults(t, output, 0, 5, 0) + securityTestUtils.VerifyJsonScanResults(t, output, 0, 4, 0) var results []services.ScanResponse err = json.Unmarshal([]byte(output), &results) diff --git a/commands/audit/jas/applicability/applicabilitymanager_test.go b/commands/audit/jas/applicability/applicabilitymanager_test.go index a7537b0f..86b8a49a 100644 --- a/commands/audit/jas/applicability/applicabilitymanager_test.go +++ b/commands/audit/jas/applicability/applicabilitymanager_test.go @@ -260,54 +260,58 @@ func TestCreateConfigFile_VerifyFileWasCreated(t *testing.T) { assert.True(t, len(fileContent) > 0) } -func TestParseResults_EmptyResults_AllCvesShouldGetUnknown(t *testing.T) { - // Arrange - scanner, cleanUp := jas.InitJasTest(t) - defer cleanUp() - - applicabilityManager := newApplicabilityScanManager(jas.FakeBasicXrayResults, mockDirectDependencies, scanner, false) - applicabilityManager.scanner.ResultsFileName = filepath.Join(jas.GetTestDataPath(), "applicability-scan", "empty-results.sarif") - - // Act - var err error - applicabilityManager.applicabilityScanResults, err = jas.ReadJasScanRunsFromFile(applicabilityManager.scanner.ResultsFileName, scanner.JFrogAppsConfig.Modules[0].SourceRoot, applicabilityDocsUrlSuffix) - - if assert.NoError(t, err) { - assert.Len(t, applicabilityManager.applicabilityScanResults, 1) - assert.Empty(t, applicabilityManager.applicabilityScanResults[0].Results) - } -} - -func TestParseResults_ApplicableCveExist(t *testing.T) { - // Arrange - scanner, cleanUp := jas.InitJasTest(t) - defer cleanUp() - applicabilityManager := newApplicabilityScanManager(jas.FakeBasicXrayResults, mockDirectDependencies, scanner, false) - applicabilityManager.scanner.ResultsFileName = filepath.Join(jas.GetTestDataPath(), "applicability-scan", "applicable-cve-results.sarif") - - // Act - var err error - applicabilityManager.applicabilityScanResults, err = jas.ReadJasScanRunsFromFile(applicabilityManager.scanner.ResultsFileName, scanner.JFrogAppsConfig.Modules[0].SourceRoot, applicabilityDocsUrlSuffix) +func TestParseResults_NewApplicabilityStatuses(t *testing.T) { + testCases := []struct { + name string + fileName string + expectedResults int + expectedApplicabilityStatuses []string + }{ + { + name: "empty results - all cves should get unknown", + fileName: "empty-results.sarif", + expectedResults: 0, + }, + { + name: "applicable cve exist", + fileName: "applicable-cve-results.sarif", + expectedResults: 2, + }, + { + name: "all cves not applicable", + fileName: "no-applicable-cves-results.sarif", + expectedResults: 5, + }, - if assert.NoError(t, err) && assert.NotNil(t, applicabilityManager.applicabilityScanResults) { - assert.Len(t, applicabilityManager.applicabilityScanResults, 1) - assert.NotEmpty(t, applicabilityManager.applicabilityScanResults[0].Results) + { + name: "new applicability statuses", + fileName: "new_ca_status.sarif", + expectedResults: 5, + expectedApplicabilityStatuses: []string{"applicable", "undetermined", "not_covered", "not_applicable"}, + }, } -} -func TestParseResults_AllCvesNotApplicable(t *testing.T) { // Arrange scanner, cleanUp := jas.InitJasTest(t) defer cleanUp() applicabilityManager := newApplicabilityScanManager(jas.FakeBasicXrayResults, mockDirectDependencies, scanner, false) - applicabilityManager.scanner.ResultsFileName = filepath.Join(jas.GetTestDataPath(), "applicability-scan", "no-applicable-cves-results.sarif") // Act - var err error - applicabilityManager.applicabilityScanResults, err = jas.ReadJasScanRunsFromFile(applicabilityManager.scanner.ResultsFileName, scanner.JFrogAppsConfig.Modules[0].SourceRoot, applicabilityDocsUrlSuffix) - - if assert.NoError(t, err) && assert.NotNil(t, applicabilityManager.applicabilityScanResults) { - assert.Len(t, applicabilityManager.applicabilityScanResults, 1) - assert.NotEmpty(t, applicabilityManager.applicabilityScanResults[0].Results) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + applicabilityManager.scanner.ResultsFileName = filepath.Join(jas.GetTestDataPath(), "applicability-scan", tc.fileName) + var err error + applicabilityManager.applicabilityScanResults, err = jas.ReadJasScanRunsFromFile(applicabilityManager.scanner.ResultsFileName, scanner.JFrogAppsConfig.Modules[0].SourceRoot, applicabilityDocsUrlSuffix) + if assert.NoError(t, err) && assert.NotNil(t, applicabilityManager.applicabilityScanResults) { + assert.Len(t, applicabilityManager.applicabilityScanResults, 1) + assert.Len(t, applicabilityManager.applicabilityScanResults[0].Results, tc.expectedResults) + if tc.name == "new applicability statuses" { + assert.Len(t, applicabilityManager.applicabilityScanResults[0].Tool.Driver.Rules, len(tc.expectedApplicabilityStatuses)) + for i, value := range tc.expectedApplicabilityStatuses { + assert.Equal(t, value, applicabilityManager.applicabilityScanResults[0].Tool.Driver.Rules[i].Properties["applicability"]) + } + } + } + }) } } diff --git a/scangraph/scangraph_test.go b/scangraph/scangraph_test.go index 28c5d19f..9368547d 100644 --- a/scangraph/scangraph_test.go +++ b/scangraph/scangraph_test.go @@ -72,7 +72,7 @@ func TestFilterResultIfNeeded(t *testing.T) { }, }, params: ScanGraphParams{ - severityLevel: 11, + severityLevel: 15, }, expected: services.ScanResponse{ Violations: []services.Violation{ diff --git a/tests/testdata/other/applicability-scan/new_ca_status.sarif b/tests/testdata/other/applicability-scan/new_ca_status.sarif new file mode 100644 index 00000000..6ed60429 --- /dev/null +++ b/tests/testdata/other/applicability-scan/new_ca_status.sarif @@ -0,0 +1,187 @@ +{ + "runs": [ + { + "tool": { + "driver": { + "name": "JFrog Applicability Scanner", + "rules": [ + { + "id": "applic_CVE-2020-14343", + "name": "CVE-2020-14343", + "properties": { + "conclusion": "negative", + "applicability": "applicable" + }, + "fullDescription": { + "text": "The scanner checks whether any of the following vulnerable functions are called:\n\n- `yaml.full_load()`\n- `yaml.load()` only unsafe calls (without specifying `SafeLoader` as the `Loader`class).", + "markdown": "The scanner checks whether any of the following vulnerable functions are called:\n\n- `yaml.full_load()`\n- `yaml.load()` only unsafe calls (without specifying `SafeLoader` as the `Loader`class)." + }, + "shortDescription": { + "text": "Scanner for CVE-2020-14343" + } + }, + { + "id": "applic_CVE-2020-1751", + "name": "CVE-2020-1751", + "properties": { + "conclusion": "UNSEEN", + "applicability": "undetermined" + } + }, + { + "id": "applic_CVE-2020-1750", + "name": "CVE-2020-1750", + "properties": { + "applicability": "not_covered" + }, + "fullDescription": { + "text": "The scanner checks whether any of the following vulnerable functions are called:\n\n- `yaml.full_load()`\n- `yaml.load()` only unsafe calls (without specifying `SafeLoader` as the `Loader`class).", + "markdown": "The scanner checks whether any of the following vulnerable functions are called:\n\n- `yaml.full_load()`\n- `yaml.load()` only unsafe calls (without specifying `SafeLoader` as the `Loader`class)." + }, + "shortDescription": { + "text": "Scanner for CVE-2020-1747" + } + }, + { + "id": "applic_CVE-2020-7788", + "name": "CVE-2020-7788", + "properties": { + "conclusion": "positive", + "applicability": "not_applicable" + }, + "fullDescription": { + "text": "The scanner checks whether the vulnerable function `ini.parse` is called.", + "markdown": "The scanner checks whether the vulnerable function `ini.parse` is called." + }, + "shortDescription": { + "text": "Scanner for CVE-2020-7788" + } + } + ], + "version": "1.0", + "informationUri": "https://jfrog.com/help/r/jfrog-security-documentation/jfrog-advanced-security" + } + }, + "invocations": [ + { + "arguments": [ + "/var/root/.jfrog/dependencies/analyzerManager/ca_scanner/applicability_scanner", + "scan", + "/tmp/jfrog.cli.temp.-1709245721-1151253638/config.yaml" + ], + "executionSuccessful": true, + "workingDirectory": { + "uri": "file:///private/var/root/.jfrog/dependencies/analyzerManager" + } + } + ], + "results": [ + { + "message": { + "text": "The vulnerable function yaml.full_load/load is called" + }, + "locations": [ + { + "physicalLocation": { + "region": { + "snippet": { + "text": "yaml.full_load(f)" + }, + "endColumn": 28, + "endLine": 4, + "startColumn": 11, + "startLine": 4 + }, + "artifactLocation": { + "uri": "file:///tmp/tmpv7yob8g_/unpacked/filesystem/blobs/sha256/dcdf8bff5b7e55b7c546dfd6085997ac9e0cc5e3c2cfe3639999dac4bc3e678e/applicable/main.py" + } + } + } + ], + "ruleId": "applic_CVE-2020-14343" + }, + { + "message": { + "text": "The vulnerable function yaml.full_load/load is called" + }, + "locations": [ + { + "physicalLocation": { + "region": { + "snippet": { + "text": "yaml.load(f, Loader=yaml.FullLoader)" + }, + "endColumn": 47, + "endLine": 4, + "startColumn": 11, + "startLine": 4 + }, + "artifactLocation": { + "uri": "file:///tmp/tmpv7yob8g_/unpacked/filesystem/blobs/sha256/dcdf8bff5b7e55b7c546dfd6085997ac9e0cc5e3c2cfe3639999dac4bc3e678e/applicable/main2.py" + } + } + } + ], + "ruleId": "applic_CVE-2020-14343" + }, + { + "message": { + "text": "The vulnerable function yaml.full_load/load is called" + }, + "locations": [ + { + "physicalLocation": { + "region": { + "snippet": { + "text": "yaml.full_load(f)" + }, + "endColumn": 28, + "endLine": 4, + "startColumn": 11, + "startLine": 4 + }, + "artifactLocation": { + "uri": "file:///tmp/tmpv7yob8g_/unpacked/filesystem/blobs/sha256/dcdf8bff5b7e55b7c546dfd6085997ac9e0cc5e3c2cfe3639999dac4bc3e678e/applicable/main.py" + } + } + } + ], + "ruleId": "applic_CVE-2020-1747" + }, + { + "message": { + "text": "The vulnerable function yaml.full_load/load is called" + }, + "locations": [ + { + "physicalLocation": { + "region": { + "snippet": { + "text": "yaml.load(f, Loader=yaml.FullLoader)" + }, + "endColumn": 47, + "endLine": 4, + "startColumn": 11, + "startLine": 4 + }, + "artifactLocation": { + "uri": "file:///tmp/tmpv7yob8g_/unpacked/filesystem/blobs/sha256/dcdf8bff5b7e55b7c546dfd6085997ac9e0cc5e3c2cfe3639999dac4bc3e678e/applicable/main2.py" + } + } + } + ], + "ruleId": "applic_CVE-2020-1747" + }, + { + "message": { + "text": "The scanner checks whether the vulnerable function `ini.parse` is called." + }, + "kind": "pass", + "ruleId": "applic_CVE-2020-7788" + } + ] + } + ], + "version": "2.1.0", + "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/cos02/schemas/sarif-schema-2.1.0.json" +} diff --git a/tests/utils/test_validation.go b/tests/utils/test_validation.go index 7c781819..bfb81759 100644 --- a/tests/utils/test_validation.go +++ b/tests/utils/test_validation.go @@ -64,22 +64,30 @@ func VerifySimpleJsonScanResults(t *testing.T, content string, minVulnerabilitie } } -func VerifySimpleJsonJasResults(t *testing.T, content string, minSastViolations, minIacViolations, minSecrets, minApplicable int) { +func VerifySimpleJsonJasResults(t *testing.T, content string, minSastViolations, minIacViolations, minSecrets, + minApplicable, minUndetermined, minNotCovered, minNotApplicable int) { var results formats.SimpleJsonResults err := json.Unmarshal([]byte(content), &results) if assert.NoError(t, err) { assert.GreaterOrEqual(t, len(results.Sast), minSastViolations, "Found less sast then expected") assert.GreaterOrEqual(t, len(results.Secrets), minSecrets, "Found less secrets then expected") assert.GreaterOrEqual(t, len(results.Iacs), minIacViolations, "Found less IaC then expected") - var applicableResults, notApplicableResults int + var applicableResults, undeterminedResults, notCoveredResults, notApplicableResults int for _, vuln := range results.Vulnerabilities { - if vuln.Applicable == string(utils.NotApplicable) { + switch vuln.Applicable { + case string(utils.NotApplicable): notApplicableResults++ - } else if vuln.Applicable == string(utils.Applicable) { + case string(utils.Applicable): applicableResults++ + case string(utils.NotCovered): + notCoveredResults++ + case string(utils.ApplicabilityUndetermined): + undeterminedResults++ } } assert.GreaterOrEqual(t, applicableResults, minApplicable, "Found less applicableResults then expected") - assert.GreaterOrEqual(t, notApplicableResults, 1, "Found less notApplicableResults then expected") + assert.GreaterOrEqual(t, undeterminedResults, minUndetermined, "Found less undeterminedResults then expected") + assert.GreaterOrEqual(t, notCoveredResults, minNotCovered, "Found less notCoveredResults then expected") + assert.GreaterOrEqual(t, notApplicableResults, minNotApplicable, "Found less notApplicableResults then expected") } } diff --git a/utils/analyzermanager.go b/utils/analyzermanager.go index adbe3fc2..464520b9 100644 --- a/utils/analyzermanager.go +++ b/utils/analyzermanager.go @@ -47,6 +47,7 @@ const ( Applicable ApplicabilityStatus = "Applicable" NotApplicable ApplicabilityStatus = "Not Applicable" ApplicabilityUndetermined ApplicabilityStatus = "Undetermined" + NotCovered ApplicabilityStatus = "Not Covered" NotScanned ApplicabilityStatus = "" ) diff --git a/utils/resultstable.go b/utils/resultstable.go index 58cb81c8..b8da7325 100644 --- a/utils/resultstable.go +++ b/utils/resultstable.go @@ -646,28 +646,33 @@ func (s *TableSeverity) printableTitle(isTable bool) string { var Severities = map[string]map[ApplicabilityStatus]*TableSeverity{ "Critical": { - Applicable: {SeverityDetails: formats.SeverityDetails{Severity: "Critical", SeverityNumValue: 15}, emoji: "💀", style: color.New(color.BgLightRed, color.LightWhite)}, - ApplicabilityUndetermined: {SeverityDetails: formats.SeverityDetails{Severity: "Critical", SeverityNumValue: 14}, emoji: "💀", style: color.New(color.BgLightRed, color.LightWhite)}, + Applicable: {SeverityDetails: formats.SeverityDetails{Severity: "Critical", SeverityNumValue: 20}, emoji: "💀", style: color.New(color.BgLightRed, color.LightWhite)}, + ApplicabilityUndetermined: {SeverityDetails: formats.SeverityDetails{Severity: "Critical", SeverityNumValue: 19}, emoji: "💀", style: color.New(color.BgLightRed, color.LightWhite)}, + NotCovered: {SeverityDetails: formats.SeverityDetails{Severity: "Critical", SeverityNumValue: 18}, emoji: "💀", style: color.New(color.BgLightRed, color.LightWhite)}, NotApplicable: {SeverityDetails: formats.SeverityDetails{Severity: "Critical", SeverityNumValue: 5}, emoji: "💀", style: color.New(color.Gray)}, }, "High": { - Applicable: {SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 13}, emoji: "🔥", style: color.New(color.Red)}, - ApplicabilityUndetermined: {SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 12}, emoji: "🔥", style: color.New(color.Red)}, + Applicable: {SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 17}, emoji: "🔥", style: color.New(color.Red)}, + ApplicabilityUndetermined: {SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 16}, emoji: "🔥", style: color.New(color.Red)}, + NotCovered: {SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 15}, emoji: "🔥", style: color.New(color.Red)}, NotApplicable: {SeverityDetails: formats.SeverityDetails{Severity: "High", SeverityNumValue: 4}, emoji: "🔥", style: color.New(color.Gray)}, }, "Medium": { - Applicable: {SeverityDetails: formats.SeverityDetails{Severity: "Medium", SeverityNumValue: 11}, emoji: "🎃", style: color.New(color.Yellow)}, - ApplicabilityUndetermined: {SeverityDetails: formats.SeverityDetails{Severity: "Medium", SeverityNumValue: 10}, emoji: "🎃", style: color.New(color.Yellow)}, + Applicable: {SeverityDetails: formats.SeverityDetails{Severity: "Medium", SeverityNumValue: 14}, emoji: "🎃", style: color.New(color.Yellow)}, + ApplicabilityUndetermined: {SeverityDetails: formats.SeverityDetails{Severity: "Medium", SeverityNumValue: 13}, emoji: "🎃", style: color.New(color.Yellow)}, + NotCovered: {SeverityDetails: formats.SeverityDetails{Severity: "Medium", SeverityNumValue: 12}, emoji: "🎃", style: color.New(color.Yellow)}, NotApplicable: {SeverityDetails: formats.SeverityDetails{Severity: "Medium", SeverityNumValue: 3}, emoji: "🎃", style: color.New(color.Gray)}, }, "Low": { - Applicable: {SeverityDetails: formats.SeverityDetails{Severity: "Low", SeverityNumValue: 9}, emoji: "👻"}, - ApplicabilityUndetermined: {SeverityDetails: formats.SeverityDetails{Severity: "Low", SeverityNumValue: 8}, emoji: "👻"}, + Applicable: {SeverityDetails: formats.SeverityDetails{Severity: "Low", SeverityNumValue: 11}, emoji: "👻"}, + ApplicabilityUndetermined: {SeverityDetails: formats.SeverityDetails{Severity: "Low", SeverityNumValue: 10}, emoji: "👻"}, + NotCovered: {SeverityDetails: formats.SeverityDetails{Severity: "Low", SeverityNumValue: 9}, emoji: "👻"}, NotApplicable: {SeverityDetails: formats.SeverityDetails{Severity: "Low", SeverityNumValue: 2}, emoji: "👻", style: color.New(color.Gray)}, }, "Unknown": { - Applicable: {SeverityDetails: formats.SeverityDetails{Severity: "Unknown", SeverityNumValue: 7}, emoji: "😐"}, - ApplicabilityUndetermined: {SeverityDetails: formats.SeverityDetails{Severity: "Unknown", SeverityNumValue: 6}, emoji: "😐"}, + Applicable: {SeverityDetails: formats.SeverityDetails{Severity: "Unknown", SeverityNumValue: 8}, emoji: "😐"}, + ApplicabilityUndetermined: {SeverityDetails: formats.SeverityDetails{Severity: "Unknown", SeverityNumValue: 7}, emoji: "😐"}, + NotCovered: {SeverityDetails: formats.SeverityDetails{Severity: "Unknown", SeverityNumValue: 6}, emoji: "😐"}, NotApplicable: {SeverityDetails: formats.SeverityDetails{Severity: "Unknown", SeverityNumValue: 1}, emoji: "😐", style: color.New(color.Gray)}, }, } @@ -699,8 +704,10 @@ func GetSeverity(severityTitle string, applicable ApplicabilityStatus) *TableSev return Severities[severityTitle][NotApplicable] case Applicable: return Severities[severityTitle][Applicable] - default: + case ApplicabilityUndetermined: return Severities[severityTitle][ApplicabilityUndetermined] + default: + return Severities[severityTitle][NotCovered] } } @@ -928,31 +935,20 @@ func convertCves(cves []services.Cve) []formats.CveRow { return cveRows } -// If at least one cve is applicable - final value is applicable -// Else if at least one cve is undetermined - final value is undetermined -// Else (case when all cves aren't applicable) -> final value is not applicable func getApplicableCveStatus(entitledForJas bool, applicabilityScanResults []*sarif.Run, cves []formats.CveRow) ApplicabilityStatus { if !entitledForJas || len(applicabilityScanResults) == 0 { return NotScanned } if len(cves) == 0 { - return ApplicabilityUndetermined + return NotCovered } - foundUndetermined := false + var applicableStatuses []ApplicabilityStatus for _, cve := range cves { if cve.Applicability != nil { - if cve.Applicability.Status == string(Applicable) { - return Applicable - } - if cve.Applicability.Status == string(ApplicabilityUndetermined) { - foundUndetermined = true - } + applicableStatuses = append(applicableStatuses, ApplicabilityStatus(cve.Applicability.Status)) } } - if foundUndetermined { - return ApplicabilityUndetermined - } - return NotApplicable + return getFinalApplicabilityStatus(applicableStatuses) } func getCveApplicabilityField(cve formats.CveRow, applicabilityScanResults []*sarif.Run, components map[string]services.Component) *formats.Applicability { @@ -962,16 +958,20 @@ func getCveApplicabilityField(cve formats.CveRow, applicabilityScanResults []*sa applicability := formats.Applicability{} resultFound := false + var applicabilityStatuses []ApplicabilityStatus for _, applicabilityRun := range applicabilityScanResults { + if rule, _ := applicabilityRun.GetRuleById(CveToApplicabilityRuleId(cve.Id)); rule != nil { + applicability.ScannerDescription = GetRuleFullDescription(rule) + status := getApplicabilityStatusFromRule(rule) + if status != "" { + applicabilityStatuses = append(applicabilityStatuses, status) + } + } result, _ := applicabilityRun.GetResultByRuleId(CveToApplicabilityRuleId(cve.Id)) if result == nil { continue } resultFound = true - rule, _ := applicabilityRun.GetRuleById(CveToApplicabilityRuleId(cve.Id)) - if rule != nil { - applicability.ScannerDescription = GetRuleFullDescription(rule) - } // Add new evidences from locations for _, location := range result.Locations { fileName := GetRelativeLocationFileName(location, applicabilityRun.Invocations) @@ -992,6 +992,8 @@ func getCveApplicabilityField(cve formats.CveRow, applicabilityScanResults []*sa } } switch { + case len(applicabilityStatuses) > 0: + applicability.Status = string(getFinalApplicabilityStatus(applicabilityStatuses)) case !resultFound: applicability.Status = string(ApplicabilityUndetermined) case len(applicability.Evidence) == 0: @@ -1048,3 +1050,54 @@ func extractDependencyNameFromComponent(key string, techIdentifier string) (depe dependencyName = split[0] return } + +func getApplicabilityStatusFromRule(rule *sarif.ReportingDescriptor) ApplicabilityStatus { + if rule.Properties["applicability"] != nil { + status, ok := rule.Properties["applicability"].(string) + if !ok { + log.Debug(fmt.Sprintf("Failed to get applicability status from rule properties for rule_id %s", rule.ID)) + } + switch status { + case "not_covered": + return NotCovered + case "undetermined": + return ApplicabilityUndetermined + case "not_applicable": + return NotApplicable + case "applicable": + return Applicable + } + } + return "" +} + +// If we don't get any statues it means the applicability scanner didn't run -> final value is not scanned +// If at least one cve is applicable -> final value is applicable +// Else if at least one cve is undetermined -> final value is undetermined +// Else if all cves are not covered -> final value is not covered +// Else (case when all cves aren't applicable) -> final value is not applicable +func getFinalApplicabilityStatus(applicabilityStatuses []ApplicabilityStatus) ApplicabilityStatus { + if len(applicabilityStatuses) == 0 { + return NotScanned + } + foundUndetermined := false + foundNotCovered := false + for _, status := range applicabilityStatuses { + if status == Applicable { + return Applicable + } + if status == ApplicabilityUndetermined { + foundUndetermined = true + } + if status == NotCovered { + foundNotCovered = true + } + } + if foundUndetermined { + return ApplicabilityUndetermined + } + if foundNotCovered { + return NotCovered + } + return NotApplicable +} diff --git a/utils/resultstable_test.go b/utils/resultstable_test.go index 5384266e..74202b57 100644 --- a/utils/resultstable_test.go +++ b/utils/resultstable_test.go @@ -427,16 +427,19 @@ func TestGetSeveritiesFormat(t *testing.T) { func TestGetApplicableCveValue(t *testing.T) { testCases := []struct { + name string scanResults *ExtendedScanResults cves []services.Cve expectedResult ApplicabilityStatus expectedCves []formats.CveRow }{ { + name: "not entitled for jas", scanResults: &ExtendedScanResults{EntitledForJas: false}, expectedResult: NotScanned, }, { + name: "no cves", scanResults: &ExtendedScanResults{ ApplicabilityScanResults: []*sarif.Run{ CreateRunWithDummyResults( @@ -447,10 +450,11 @@ func TestGetApplicableCveValue(t *testing.T) { EntitledForJas: true, }, cves: nil, - expectedResult: ApplicabilityUndetermined, + expectedResult: NotCovered, expectedCves: nil, }, { + name: "applicable cve", scanResults: &ExtendedScanResults{ ApplicabilityScanResults: []*sarif.Run{ CreateRunWithDummyResults( @@ -465,6 +469,7 @@ func TestGetApplicableCveValue(t *testing.T) { expectedCves: []formats.CveRow{{Id: "testCve2", Applicability: &formats.Applicability{Status: string(Applicable)}}}, }, { + name: "undetermined cve", scanResults: &ExtendedScanResults{ ApplicabilityScanResults: []*sarif.Run{ CreateRunWithDummyResults( @@ -479,6 +484,7 @@ func TestGetApplicableCveValue(t *testing.T) { expectedCves: []formats.CveRow{{Id: "testCve3"}}, }, { + name: "not applicable cve", scanResults: &ExtendedScanResults{ ApplicabilityScanResults: []*sarif.Run{ CreateRunWithDummyResults( @@ -493,6 +499,7 @@ func TestGetApplicableCveValue(t *testing.T) { expectedCves: []formats.CveRow{{Id: "testCve1", Applicability: &formats.Applicability{Status: string(NotApplicable)}}, {Id: "testCve2", Applicability: &formats.Applicability{Status: string(NotApplicable)}}}, }, { + name: "applicable and not applicable cves", scanResults: &ExtendedScanResults{ ApplicabilityScanResults: []*sarif.Run{ CreateRunWithDummyResults( @@ -507,6 +514,7 @@ func TestGetApplicableCveValue(t *testing.T) { expectedCves: []formats.CveRow{{Id: "testCve1", Applicability: &formats.Applicability{Status: string(NotApplicable)}}, {Id: "testCve2", Applicability: &formats.Applicability{Status: string(Applicable)}}}, }, { + name: "undetermined and not applicable cves", scanResults: &ExtendedScanResults{ ApplicabilityScanResults: []*sarif.Run{ CreateRunWithDummyResults(CreateDummyPassingResult("applic_testCve1")), @@ -516,6 +524,50 @@ func TestGetApplicableCveValue(t *testing.T) { expectedResult: ApplicabilityUndetermined, expectedCves: []formats.CveRow{{Id: "testCve1", Applicability: &formats.Applicability{Status: string(NotApplicable)}}, {Id: "testCve2"}}, }, + { + name: "new scan statuses - applicable wins all statuses", + scanResults: &ExtendedScanResults{ + ApplicabilityScanResults: []*sarif.Run{ + CreateRunWithDummyResultAndRuleProperties("applicability", "applicable", CreateDummyPassingResult("applic_testCve1")), + CreateRunWithDummyResultAndRuleProperties("applicability", "not_applicable", CreateDummyPassingResult("applic_testCve2")), + CreateRunWithDummyResultAndRuleProperties("applicability", "not_covered", CreateDummyPassingResult("applic_testCve3")), + }, + EntitledForJas: true}, + cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}, {Id: "testCve3"}}, + expectedResult: Applicable, + expectedCves: []formats.CveRow{{Id: "testCve1", Applicability: &formats.Applicability{Status: string(Applicable)}}, + {Id: "testCve2", Applicability: &formats.Applicability{Status: string(NotApplicable)}}, + {Id: "testCve2", Applicability: &formats.Applicability{Status: string(NotCovered)}}, + }, + }, + { + name: "new scan statuses - not covered wins not applicable", + scanResults: &ExtendedScanResults{ + ApplicabilityScanResults: []*sarif.Run{ + CreateRunWithDummyResultAndRuleProperties("applicability", "not_covered", CreateDummyPassingResult("applic_testCve1")), + CreateRunWithDummyResultAndRuleProperties("applicability", "not_applicable", CreateDummyPassingResult("applic_testCve2")), + }, + EntitledForJas: true}, + cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}}, + expectedResult: NotCovered, + expectedCves: []formats.CveRow{{Id: "testCve1", Applicability: &formats.Applicability{Status: string(NotCovered)}}, + {Id: "testCve2", Applicability: &formats.Applicability{Status: string(NotApplicable)}}, + }, + }, + { + name: "new scan statuses - undetermined wins not covered", + scanResults: &ExtendedScanResults{ + ApplicabilityScanResults: []*sarif.Run{ + CreateRunWithDummyResultAndRuleProperties("applicability", "not_covered", CreateDummyPassingResult("applic_testCve1")), + CreateRunWithDummyResultAndRuleProperties("applicability", "undetermined", CreateDummyPassingResult("applic_testCve2")), + }, + EntitledForJas: true}, + cves: []services.Cve{{Id: "testCve1"}, {Id: "testCve2"}}, + expectedResult: ApplicabilityUndetermined, + expectedCves: []formats.CveRow{{Id: "testCve1", Applicability: &formats.Applicability{Status: string(NotCovered)}}, + {Id: "testCve2", Applicability: &formats.Applicability{Status: string(ApplicabilityUndetermined)}}, + }, + }, } for _, testCase := range testCases { @@ -750,7 +802,7 @@ func TestPrepareIac(t *testing.T) { { SeverityDetails: formats.SeverityDetails{ Severity: "High", - SeverityNumValue: 13, + SeverityNumValue: 17, }, Finding: "other iac finding", Location: formats.Location{ @@ -765,7 +817,7 @@ func TestPrepareIac(t *testing.T) { { SeverityDetails: formats.SeverityDetails{ Severity: "Medium", - SeverityNumValue: 11, + SeverityNumValue: 14, }, Finding: "iac finding", Location: formats.Location{ @@ -780,7 +832,7 @@ func TestPrepareIac(t *testing.T) { { SeverityDetails: formats.SeverityDetails{ Severity: "Medium", - SeverityNumValue: 11, + SeverityNumValue: 14, }, Finding: "iac finding", Location: formats.Location{ @@ -847,7 +899,7 @@ func TestPrepareSecrets(t *testing.T) { { SeverityDetails: formats.SeverityDetails{ Severity: "Low", - SeverityNumValue: 9, + SeverityNumValue: 11, }, Finding: "other secret finding", Location: formats.Location{ @@ -862,7 +914,7 @@ func TestPrepareSecrets(t *testing.T) { { SeverityDetails: formats.SeverityDetails{ Severity: "Medium", - SeverityNumValue: 11, + SeverityNumValue: 14, }, Finding: "secret finding", Location: formats.Location{ @@ -877,7 +929,7 @@ func TestPrepareSecrets(t *testing.T) { { SeverityDetails: formats.SeverityDetails{ Severity: "Medium", - SeverityNumValue: 11, + SeverityNumValue: 14, }, Finding: "secret finding", Location: formats.Location{ @@ -953,7 +1005,7 @@ func TestPrepareSast(t *testing.T) { { SeverityDetails: formats.SeverityDetails{ Severity: "High", - SeverityNumValue: 13, + SeverityNumValue: 17, }, Finding: "other sast finding", Location: formats.Location{ @@ -968,7 +1020,7 @@ func TestPrepareSast(t *testing.T) { { SeverityDetails: formats.SeverityDetails{ Severity: "Medium", - SeverityNumValue: 11, + SeverityNumValue: 14, }, Finding: "sast finding", Location: formats.Location{ @@ -1021,7 +1073,7 @@ func TestPrepareSast(t *testing.T) { { SeverityDetails: formats.SeverityDetails{ Severity: "Medium", - SeverityNumValue: 11, + SeverityNumValue: 14, }, Finding: "sast finding", Location: formats.Location{ @@ -1044,6 +1096,45 @@ func TestPrepareSast(t *testing.T) { } } +func TestGetFinalApplicabilityStatus(t *testing.T) { + testCases := []struct { + name string + input []ApplicabilityStatus + expectedOutput ApplicabilityStatus + }{ + { + name: "applicable wins all statuses", + input: []ApplicabilityStatus{ApplicabilityUndetermined, Applicable, NotCovered, NotApplicable}, + expectedOutput: Applicable, + }, + { + name: "undetermined wins not covered", + input: []ApplicabilityStatus{NotCovered, ApplicabilityUndetermined, NotCovered, NotApplicable}, + expectedOutput: ApplicabilityUndetermined, + }, + { + name: "not covered wins not applicable", + input: []ApplicabilityStatus{NotApplicable, NotCovered, NotApplicable}, + expectedOutput: NotCovered, + }, + { + name: "all statuses are not applicable", + input: []ApplicabilityStatus{NotApplicable, NotApplicable, NotApplicable}, + expectedOutput: NotApplicable, + }, + { + name: "no statuses", + input: []ApplicabilityStatus{}, + expectedOutput: NotScanned, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expectedOutput, getFinalApplicabilityStatus(tc.input)) + }) + } +} + func newBoolPtr(v bool) *bool { return &v } diff --git a/utils/resultwriter_test.go b/utils/resultwriter_test.go index 87279153..32697020 100644 --- a/utils/resultwriter_test.go +++ b/utils/resultwriter_test.go @@ -64,7 +64,7 @@ func TestGetSarifTableDescription(t *testing.T) { name string formattedDeps string maxCveScore string - applicable string + status ApplicabilityStatus fixedVersions []string expectedDescription string }{ @@ -72,15 +72,15 @@ func TestGetSarifTableDescription(t *testing.T) { name: "Applicable vulnerability", formattedDeps: "`example-package 1.0.0`", maxCveScore: "7.5", - applicable: "Applicable", + status: "Applicable", fixedVersions: []string{"1.0.1", "1.0.2"}, expectedDescription: "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 7.5 | Applicable | `example-package 1.0.0` | 1.0.1, 1.0.2 |", }, { - name: "Non-applicable vulnerability", + name: "Not-scanned vulnerability", formattedDeps: "`example-package 2.0.0`", maxCveScore: "6.2", - applicable: "", + status: "", fixedVersions: []string{"2.0.1"}, expectedDescription: "| Severity Score | Direct Dependencies | Fixed Versions |\n| :---: | :----: | :---: |\n| 6.2 | `example-package 2.0.0` | 2.0.1 |", }, @@ -88,15 +88,39 @@ func TestGetSarifTableDescription(t *testing.T) { name: "No fixed versions", formattedDeps: "`example-package 3.0.0`", maxCveScore: "3.0", - applicable: "", + status: "", fixedVersions: []string{}, expectedDescription: "| Severity Score | Direct Dependencies | Fixed Versions |\n| :---: | :----: | :---: |\n| 3.0 | `example-package 3.0.0` | No fix available |", }, + { + name: "Not-covered vulnerability", + formattedDeps: "`example-package 3.0.0`", + maxCveScore: "3.0", + status: "Not covered", + fixedVersions: []string{"3.0.1"}, + expectedDescription: "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 3.0 | Not covered | `example-package 3.0.0` | 3.0.1 |", + }, + { + name: "Undetermined vulnerability", + formattedDeps: "`example-package 3.0.0`", + maxCveScore: "3.0", + status: "Undetermined", + fixedVersions: []string{"3.0.1"}, + expectedDescription: "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 3.0 | Undetermined | `example-package 3.0.0` | 3.0.1 |", + }, + { + name: "Not-status vulnerability", + formattedDeps: "`example-package 3.0.0`", + maxCveScore: "3.0", + status: "Not status", + fixedVersions: []string{"3.0.1"}, + expectedDescription: "| Severity Score | Contextual Analysis | Direct Dependencies | Fixed Versions |\n| :---: | :---: | :---: | :---: |\n| 3.0 | Not status | `example-package 3.0.0` | 3.0.1 |", + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - output := getSarifTableDescription(tc.formattedDeps, tc.maxCveScore, tc.applicable, tc.fixedVersions) + output := getSarifTableDescription(tc.formattedDeps, tc.maxCveScore, tc.status.String(), tc.fixedVersions) assert.Equal(t, tc.expectedDescription, output) }) } diff --git a/utils/test_sarifutils.go b/utils/test_sarifutils.go index 5034b1eb..1404d08a 100644 --- a/utils/test_sarifutils.go +++ b/utils/test_sarifutils.go @@ -13,6 +13,17 @@ func CreateRunWithDummyResults(results ...*sarif.Result) *sarif.Run { return run } +func CreateRunWithDummyResultAndRuleProperties(property, value string, result *sarif.Result) *sarif.Run { + run := sarif.NewRunWithInformationURI("", "") + if result.RuleID != nil { + run.AddRule(*result.RuleID) + } + run.AddResult(result) + run.Tool.Driver.Rules[0].Properties = make(sarif.Properties) + run.Tool.Driver.Rules[0].Properties[property] = value + return run +} + func CreateResultWithLocations(msg, ruleId, level string, locations ...*sarif.Location) *sarif.Result { return &sarif.Result{ Message: *sarif.NewTextMessage(msg),