From e5f675e2d1c250e6188be20b29e404bdab696a74 Mon Sep 17 00:00:00 2001 From: Assaf Attias <49212512+attiasas@users.noreply.github.com> Date: Fri, 30 Aug 2024 15:43:36 +0300 Subject: [PATCH] Improve Job Summary (#137) --- cli/scancommands.go | 2 +- cli/scancommands_test.go | 2 +- commands/curation/curationaudit.go | 50 +- commands/curation/curationaudit_test.go | 45 +- commands/scan/buildscan.go | 8 +- commands/scan/dockerscan.go | 15 +- commands/scan/scan.go | 13 +- formats/summary.go | 377 +++++--- formats/summary_test.go | 464 +++++++--- go.mod | 12 +- go.sum | 24 +- resources/resources.go | 3 + resources/statusIcons/failed.svg | 1 + resources/statusIcons/passed.svg | 1 + .../jobSummary/binary_vulnerabilities.md | 1 + .../jobSummary/build_scan_vulnerabilities.md | 1 + .../jobSummary/docker_vulnerabilities.md | 1 + .../other/jobSummary/multi_command_job.md | 21 - .../other/jobSummary/no_violations.md | 1 + .../other/jobSummary/no_vulnerabilities.md | 1 + .../other/jobSummary/security_section.md | 8 + .../testdata/other/jobSummary/single_issue.md | 1 - .../other/jobSummary/single_no_issue.md | 3 - tests/testdata/other/jobSummary/violations.md | 1 + .../jobSummary/violations_not_defined.md | 1 + .../violations_not_extended_view.md | 1 + utils/results.go | 39 +- utils/results_test.go | 159 +--- utils/resultstable.go | 6 +- utils/resultwriter.go | 198 ++++ utils/resultwriter_test.go | 152 ++++ utils/securityJobSummary.go | 848 +++++++++--------- utils/securityJobSummary_test.go | 519 ++++++++--- utils/severityutils/severity.go | 20 + utils/test_mocks.go | 2 + utils/utils.go | 10 + utils/xsc/analyticsmetrics.go | 2 +- 37 files changed, 1936 insertions(+), 1077 deletions(-) create mode 100644 resources/resources.go create mode 100644 resources/statusIcons/failed.svg create mode 100644 resources/statusIcons/passed.svg create mode 100644 tests/testdata/other/jobSummary/binary_vulnerabilities.md create mode 100644 tests/testdata/other/jobSummary/build_scan_vulnerabilities.md create mode 100644 tests/testdata/other/jobSummary/docker_vulnerabilities.md delete mode 100644 tests/testdata/other/jobSummary/multi_command_job.md create mode 100644 tests/testdata/other/jobSummary/no_violations.md create mode 100644 tests/testdata/other/jobSummary/no_vulnerabilities.md create mode 100644 tests/testdata/other/jobSummary/security_section.md delete mode 100644 tests/testdata/other/jobSummary/single_issue.md delete mode 100644 tests/testdata/other/jobSummary/single_no_issue.md create mode 100644 tests/testdata/other/jobSummary/violations.md create mode 100644 tests/testdata/other/jobSummary/violations_not_defined.md create mode 100644 tests/testdata/other/jobSummary/violations_not_extended_view.md diff --git a/cli/scancommands.go b/cli/scancommands.go index c969eadb..17ea3e9a 100644 --- a/cli/scancommands.go +++ b/cli/scancommands.go @@ -563,7 +563,7 @@ func ShouldRunCurationAfterFailure(c *components.Context, tech techutils.Technol if !IsSupportedCommandForCurationInspect(cmdName) { return } - if os.Getenv(coreutils.OutputDirPathEnv) == "" || + if os.Getenv(coreutils.SummaryOutputDirPathEnv) == "" || os.Getenv(SkipCurationAfterFailureEnv) == "true" { return } diff --git a/cli/scancommands_test.go b/cli/scancommands_test.go index 7ce82286..3e829ed4 100644 --- a/cli/scancommands_test.go +++ b/cli/scancommands_test.go @@ -95,7 +95,7 @@ func TestShouldRunCurationAfterFailure(t *testing.T) { defer callBack() } if tt.envOutputDirPath != "" { - callBack2 := clienttestutils.SetEnvWithCallbackAndAssert(t, coreutils.OutputDirPathEnv, tt.envOutputDirPath) + callBack2 := clienttestutils.SetEnvWithCallbackAndAssert(t, coreutils.SummaryOutputDirPathEnv, tt.envOutputDirPath) defer callBack2() } diff --git a/commands/curation/curationaudit.go b/commands/curation/curationaudit.go index 68beaae0..65b988a6 100644 --- a/commands/curation/curationaudit.go +++ b/commands/curation/curationaudit.go @@ -4,6 +4,8 @@ import ( "encoding/json" "errors" "fmt" + "golang.org/x/exp/maps" + "github.com/jfrog/gofrog/datastructures" "github.com/jfrog/gofrog/parallel" rtUtils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" @@ -240,43 +242,43 @@ func (ca *CurationAuditCommand) Run() (err error) { for projectPath, packagesStatus := range results { err = errors.Join(err, printResult(ca.OutputFormat(), projectPath, packagesStatus.packagesStatus)) } - - err = errors.Join(err, utils.RecordSecurityCommandOutput(utils.ScanCommandSummaryResult{Results: convertResultsToSummary(results), Section: utils.Curation})) + err = errors.Join(err, utils.RecordSecurityCommandSummary(utils.NewCurationSummary(convertResultsToSummary(results)))) return } -func convertResultsToSummary(results map[string]*CurationReport) formats.SummaryResults { - summaryResults := formats.SummaryResults{} +func convertResultsToSummary(results map[string]*CurationReport) formats.ResultsSummary { + summaryResults := formats.ResultsSummary{} for projectPath, packagesStatus := range results { - blocked := convertBlocked(packagesStatus.packagesStatus) - approved := packagesStatus.totalNumberOfPackages - blocked.GetCountOfKeys(false) - - summaryResults.Scans = append(summaryResults.Scans, formats.ScanSummaryResult{Target: projectPath, + summaryResults.Scans = append(summaryResults.Scans, formats.ScanSummary{Target: projectPath, CuratedPackages: &formats.CuratedPackages{ - Blocked: blocked, - Approved: approved, - }}) + PackageCount: packagesStatus.totalNumberOfPackages, + Blocked: getBlocked(packagesStatus.packagesStatus), + }, + }) } return summaryResults } -func convertBlocked(pkgStatus []*PackageStatus) formats.TwoLevelSummaryCount { - blocked := formats.TwoLevelSummaryCount{} +func getBlocked(pkgStatus []*PackageStatus) []formats.BlockedPackages { + blockedMap := map[string]formats.BlockedPackages{} for _, pkg := range pkgStatus { for _, policy := range pkg.Policy { - polAndCond := formatPolicyAndCond(policy.Policy, policy.Condition) - if _, ok := blocked[polAndCond]; !ok { - blocked[polAndCond] = formats.SummaryCount{} + polAndCondKey := getPolicyAndConditionId(policy.Policy, policy.Condition) + if _, ok := blockedMap[polAndCondKey]; !ok { + blockedMap[polAndCondKey] = formats.BlockedPackages{ + Policy: policy.Policy, + Condition: policy.Condition, + Packages: make(map[string]int), + } } uniqId := getPackageId(pkg.PackageName, pkg.PackageVersion) - blocked[polAndCond][uniqId]++ + if _, ok := blockedMap[polAndCondKey].Packages[uniqId]; !ok { + blockedMap[polAndCondKey].Packages[uniqId] = 0 + } + blockedMap[polAndCondKey].Packages[uniqId]++ } } - return blocked -} - -func formatPolicyAndCond(policy, cond string) string { - return fmt.Sprintf("Policy: %s, Condition: %s", policy, cond) + return maps.Values(blockedMap) } // The unique identifier of a package includes the package name with its version @@ -284,6 +286,10 @@ func getPackageId(packageName, packageVersion string) string { return fmt.Sprintf("%s:%s", packageName, packageVersion) } +func getPolicyAndConditionId(policy, condition string) string { + return fmt.Sprintf("%s:%s", policy, condition) +} + func (ca *CurationAuditCommand) doCurateAudit(results map[string]*CurationReport) error { techs := techutils.DetectedTechnologiesList() for _, tech := range techs { diff --git a/commands/curation/curationaudit_test.go b/commands/curation/curationaudit_test.go index 5777bc91..a61a5b84 100644 --- a/commands/curation/curationaudit_test.go +++ b/commands/curation/curationaudit_test.go @@ -857,7 +857,7 @@ func Test_convertResultsToSummary(t *testing.T) { tests := []struct { name string input map[string]*CurationReport - expected formats.SummaryResults + expected formats.ResultsSummary }{ { name: "results for one result", @@ -882,17 +882,17 @@ func Test_convertResultsToSummary(t *testing.T) { totalNumberOfPackages: 5, }, }, - expected: formats.SummaryResults{ - Scans: []formats.ScanSummaryResult{ + expected: formats.ResultsSummary{ + Scans: []formats.ScanSummary{ { Target: "project1", CuratedPackages: &formats.CuratedPackages{ - Blocked: formats.TwoLevelSummaryCount{ - formatPolicyAndCond("policy1", "cond1"): formats.SummaryCount{ - getPackageId("test1", "1.0.0"): 1, - }, - }, - Approved: 4, + PackageCount: 5, + Blocked: []formats.BlockedPackages{{ + Policy: "policy1", + Condition: "cond1", + Packages: map[string]int{"test1:1.0.0": 1}, + }}, }, }, }, @@ -950,25 +950,27 @@ func Test_convertResultsToSummary(t *testing.T) { }, }, }, - totalNumberOfPackages: 5, + totalNumberOfPackages: 6, }, }, - expected: formats.SummaryResults{ - Scans: []formats.ScanSummaryResult{ + expected: formats.ResultsSummary{ + Scans: []formats.ScanSummary{ { Target: "project1", CuratedPackages: &formats.CuratedPackages{ - Blocked: formats.TwoLevelSummaryCount{ - formatPolicyAndCond("policy1", "cond1"): formats.SummaryCount{ - getPackageId("test1", "1.0.0"): 1, + PackageCount: 6, + Blocked: []formats.BlockedPackages{ + { + Policy: "policy1", + Condition: "cond1", + Packages: map[string]int{"test1:1.0.0": 1}, }, - formatPolicyAndCond("policy2", "cond2"): formats.SummaryCount{ - getPackageId("test1", "1.0.0"): 1, - getPackageId("test2", "2.0.0"): 1, - getPackageId("test3", "3.0.0"): 1, + { + Policy: "policy2", + Condition: "cond2", + Packages: map[string]int{"test1:1.0.0": 1, "test2:2.0.0": 1, "test3:3.0.0": 1}, }, }, - Approved: 2, }, }, }, @@ -977,8 +979,7 @@ func Test_convertResultsToSummary(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - results := convertResultsToSummary(tt.input) - assert.Equal(t, tt.expected, results) + assert.ElementsMatch(t, tt.expected.Scans, convertResultsToSummary(tt.input).Scans) }) } } diff --git a/commands/scan/buildscan.go b/commands/scan/buildscan.go index 66ad5d90..40149491 100644 --- a/commands/scan/buildscan.go +++ b/commands/scan/buildscan.go @@ -183,7 +183,13 @@ func (bsc *BuildScanCommand) runBuildScanAndPrintResults(xrayManager *xray.XrayS } } } - err = utils.RecordSecurityCommandOutput(utils.ScanCommandSummaryResult{Results: scanResults.GetSummary(), Section: utils.Build}) + err = utils.RecordSecurityCommandSummary(utils.NewBuildScanSummary( + scanResults, + bsc.serverDetails, + bsc.includeVulnerabilities, + bsc.buildConfiguration.GetProject() != "", + params.BuildName, params.BuildNumber, + )) return } diff --git a/commands/scan/dockerscan.go b/commands/scan/dockerscan.go index 2c6e27a6..1ba036e4 100644 --- a/commands/scan/dockerscan.go +++ b/commands/scan/dockerscan.go @@ -97,15 +97,18 @@ func (dsc *DockerScanCommand) Run() (err error) { } }() return dsc.ScanCommand.RunAndRecordResults(func(scanResults *utils.Results) (err error) { - if scanResults == nil || len(scanResults.ScaResults) == 0 { + if scanResults == nil { return } - for i := range scanResults.ScaResults { - // Set the image tag as the target for the scan results (will show `image.tar` as target if not set) - scanResults.ScaResults[i].Target = dsc.imageTag - } dsc.analyticsMetricsService.UpdateGeneralEvent(dsc.analyticsMetricsService.CreateXscAnalyticsGeneralEventFinalizeFromAuditResults(scanResults)) - return utils.RecordSecurityCommandOutput(utils.ScanCommandSummaryResult{Results: scanResults.GetSummary(), Section: utils.Binary}) + + return utils.RecordSecurityCommandSummary(utils.NewDockerScanSummary( + scanResults, + dsc.ScanCommand.serverDetails, + dsc.ScanCommand.includeVulnerabilities, + hasViolationContext(dsc.ScanCommand.watches, dsc.ScanCommand.projectKey), + dsc.imageTag, + )) }) } diff --git a/commands/scan/scan.go b/commands/scan/scan.go index a98cfbc2..6f0ba747 100644 --- a/commands/scan/scan.go +++ b/commands/scan/scan.go @@ -191,10 +191,19 @@ func (scanCmd *ScanCommand) indexFile(filePath string) (*xrayUtils.BinaryGraphNo func (scanCmd *ScanCommand) Run() (err error) { return scanCmd.RunAndRecordResults(func(scanResults *utils.Results) error { - return utils.RecordSecurityCommandOutput(utils.ScanCommandSummaryResult{Results: scanResults.GetSummary(), Section: utils.Binary}) + return utils.RecordSecurityCommandSummary(utils.NewBinaryScanSummary( + scanResults, + scanCmd.serverDetails, + scanCmd.includeVulnerabilities, + hasViolationContext(scanCmd.watches, scanCmd.projectKey), + )) }) } +func hasViolationContext(watches []string, projectKey string) bool { + return len(watches) > 0 || projectKey != "" +} + func (scanCmd *ScanCommand) RunAndRecordResults(recordResFunc func(scanResults *utils.Results) error) (err error) { defer func() { if err != nil { @@ -312,7 +321,7 @@ func (scanCmd *ScanCommand) RunAndRecordResults(recordResFunc func(scanResults * SetIncludeVulnerabilities(scanCmd.includeVulnerabilities). SetIncludeLicenses(scanCmd.includeLicenses). SetPrintExtendedTable(scanCmd.printExtendedTable). - SetIsMultipleRootProject(true). + SetIsMultipleRootProject(scanResults.IsMultipleProject()). SetScanType(services.Binary). PrintScanResults(); err != nil { return diff --git a/formats/summary.go b/formats/summary.go index ce546aaa..4e16fe0c 100644 --- a/formats/summary.go +++ b/formats/summary.go @@ -2,193 +2,334 @@ package formats import ( "github.com/jfrog/gofrog/datastructures" + "golang.org/x/exp/slices" ) const ( - ScaScan SummarySubScanType = "SCA" - IacScan SummarySubScanType = "IAC" - SecretsScan SummarySubScanType = "Secrets" - SastScan SummarySubScanType = "SAST" - - ViolationTypeSecurity ViolationIssueType = "security" - ViolationTypeLicense ViolationIssueType = "license" - ViolationTypeOperationalRisk ViolationIssueType = "operational_risk" + IacResult SummaryResultType = "IAC" + SecretsResult SummaryResultType = "Secrets" + SastResult SummaryResultType = "SAST" + ScaResult SummaryResultType = "SCA" + ScaSecurityResult SummaryResultType = "Security" + ScaLicenseResult SummaryResultType = "License" + ScaOperationalResult SummaryResultType = "Operational" + + NoStatus = "" ) -type SummarySubScanType string -type ViolationIssueType string +type SummaryResultType string -func (v ViolationIssueType) String() string { - return string(v) +func (srt SummaryResultType) String() string { + return string(srt) } -type SummaryResults struct { - Scans []ScanSummaryResult `json:"scans"` +type ResultsSummary struct { + Scans []ScanSummary `json:"scans"` } -func (sr SummaryResults) GetTotalIssueCount() (total int) { - for _, scan := range sr.Scans { - total += scan.GetTotalIssueCount() - } - return +type ScanSummary struct { + Target string `json:"target"` + Vulnerabilities *ScanResultSummary `json:"vulnerabilities,omitempty"` + Violations *ScanViolationsSummary `json:"violations,omitempty"` + CuratedPackages *CuratedPackages `json:"curated,omitempty"` } -type ScanSummaryResult struct { - Target string `json:"target,omitempty"` - Vulnerabilities *ScanVulnerabilitiesSummary `json:"vulnerabilities,omitempty"` - Violations TwoLevelSummaryCount `json:"violations,omitempty"` - CuratedPackages *CuratedPackages `json:"curated,omitempty"` +type ScanResultSummary struct { + ScaResults *ScaScanResultSummary `json:"sca,omitempty"` + IacResults *ResultSummary `json:"iac,omitempty"` + SecretsResults *ResultSummary `json:"secrets,omitempty"` + SastResults *ResultSummary `json:"sast,omitempty"` } -type CuratedPackages struct { - Blocked TwoLevelSummaryCount `json:"blocked,omitempty"` - Approved int `json:"approved,omitempty"` +type ScanViolationsSummary struct { + Watches []string `json:"watches,omitempty"` + FailBuild bool `json:"fail_build,omitempty"` + ScanResultSummary } -type ScanVulnerabilitiesSummary struct { - ScaScanResults *ScanScaResult `json:"sca,omitempty"` - IacScanResults *SummaryCount `json:"iac,omitempty"` - SecretsScanResults *SummaryCount `json:"secrets,omitempty"` - SastScanResults *SummaryCount `json:"sast,omitempty"` +type ScaScanResultSummary struct { + ScanIds []string `json:"scan_ids,omitempty"` + MoreInfoUrls []string `json:"more_info_urls,omitempty"` + Security ResultSummary `json:"security,omitempty"` + License ResultSummary `json:"license,omitempty"` + OperationalRisk ResultSummary `json:"operational_risk,omitempty"` } -type ScanScaResult struct { - SummaryCount TwoLevelSummaryCount `json:"sca,omitempty"` - UniqueFindings int `json:"unique_findings,omitempty"` +type CuratedPackages struct { + Blocked []BlockedPackages `json:"blocked,omitempty"` + PackageCount int `json:"num_packages,omitempty"` +} + +type BlockedPackages struct { + Policy string `json:"policy,omitempty"` + Condition string `json:"condition,omitempty"` + Packages map[string]int `json:"packages"` } -func (s *ScanSummaryResult) HasIssues() bool { - return s.HasViolations() || s.HasSecurityVulnerabilities() || s.HasBlockedCuration() +func (cp *CuratedPackages) GetApprovedCount() int { + return cp.PackageCount - cp.GetBlockedCount() } -func (s *ScanSummaryResult) HasViolations() bool { - return s.Violations.GetTotal() > 0 +func (cp *CuratedPackages) GetBlockedCount() int { + parsed := datastructures.MakeSet[string]() + for _, blocked := range cp.Blocked { + for packageId, _ := range blocked.Packages { + if parsed.Exists(packageId) { + continue + } + parsed.Add(packageId) + } + } + return parsed.Size() } -func (s *ScanSummaryResult) HasSecurityVulnerabilities() bool { - return s.Vulnerabilities != nil && s.Vulnerabilities.GetTotalIssueCount() > 0 +// Severity -> status -> Count +type ResultSummary map[string]map[string]int + +func (rs ResultSummary) GetTotal(filterSeverities ...string) (total int) { + for severity, count := range rs { + if len(filterSeverities) > 0 && !slices.Contains(filterSeverities, severity) { + continue + } + for _, c := range count { + total += c + } + } + return } -func (s *ScanSummaryResult) HasBlockedCuration() bool { - return s.CuratedPackages != nil && s.CuratedPackages.Blocked.GetTotal() > 0 +func (rs *ResultsSummary) HasViolations() bool { + for _, scan := range rs.Scans { + if scan.HasViolations() { + return true + } + } + return false } -func (s *ScanSummaryResult) GetTotalIssueCount() (total int) { - if s.Vulnerabilities != nil { - total += s.Vulnerabilities.GetTotalIssueCount() +func (rs *ResultsSummary) GetTotalVulnerabilities(filterTypes ...SummaryResultType) (total int) { + for _, scan := range rs.Scans { + if scan.Vulnerabilities != nil { + total += scan.Vulnerabilities.GetTotal(filterTypes...) + } } - total += s.Violations.GetTotal() return +} +func (rs *ResultsSummary) GetTotalViolations(filterTypes ...SummaryResultType) (total int) { + for _, scan := range rs.Scans { + if scan.Violations != nil { + total += scan.Violations.GetTotal(filterTypes...) + } + } + return } -func (s *ScanSummaryResult) GetTotalViolationCount() (total int) { - return s.Violations.GetTotal() +func (sc *ScanSummary) HasCuratedPackages() bool { + return sc.CuratedPackages != nil } -func (s *ScanVulnerabilitiesSummary) GetTotalUniqueIssueCount() (total int) { - return s.getTotalIssueCount(true) +func (sc *ScanSummary) HasBlockedPackages() bool { + return sc.CuratedPackages != nil && len(sc.CuratedPackages.Blocked) > 0 } -func (s *ScanVulnerabilitiesSummary) GetTotalIssueCount() (total int) { - return s.getTotalIssueCount(false) +func (sc *ScanSummary) HasViolations() bool { + return sc.Violations != nil && sc.Violations.GetTotal() > 0 } -func (s *CuratedPackages) GetTotalPackages() int { - return s.Approved + s.Blocked.GetCountOfKeys(false) +func (sc *ScanSummary) HasVulnerabilities() bool { + return sc.Vulnerabilities != nil && sc.Vulnerabilities.GetTotal() > 0 } -func (s *ScanVulnerabilitiesSummary) getTotalIssueCount(unique bool) (total int) { - if s.ScaScanResults != nil { - if unique { - total += s.ScaScanResults.UniqueFindings - } else { - total += s.ScaScanResults.SummaryCount.GetTotal() - } +func (sc *ScanSummary) GetScanIds() (scanIds []string) { + if sc.Vulnerabilities != nil { + scanIds = append(scanIds, sc.Vulnerabilities.GetScanIds()...) } - if s.IacScanResults != nil { - total += s.IacScanResults.GetTotal() + if sc.Violations != nil { + scanIds = append(scanIds, sc.Violations.GetScanIds()...) } - if s.SecretsScanResults != nil { - total += s.SecretsScanResults.GetTotal() + return +} + +func (srs *ScanResultSummary) GetMoreInfoUrls() (urls []string) { + if srs.ScaResults != nil { + for _, url := range srs.ScaResults.MoreInfoUrls { + if url != "" { + urls = append(urls, url) + } + } } - if s.SastScanResults != nil { - total += s.SastScanResults.GetTotal() + return +} + +func (srs *ScanResultSummary) GetScanIds() (scanIds []string) { + if srs.ScaResults != nil { + scanIds = append(scanIds, srs.ScaResults.ScanIds...) } return } -func (s *ScanVulnerabilitiesSummary) GetSubScansWithIssues() []SummarySubScanType { - subScans := []SummarySubScanType{} - if s.SecretsScanResults != nil && s.SecretsScanResults.GetTotal() > 0 { - subScans = append(subScans, SecretsScan) +func (srs *ScanResultSummary) HasIssues() bool { + return srs.GetTotal() > 0 +} + +func (srs *ScanResultSummary) GetTotal(filterTypes ...SummaryResultType) (total int) { + if srs.IacResults != nil && isFilterApply(IacResult, filterTypes) { + total += srs.IacResults.GetTotal() } - if s.SastScanResults != nil && s.SastScanResults.GetTotal() > 0 { - subScans = append(subScans, SastScan) + if srs.SecretsResults != nil && isFilterApply(SecretsResult, filterTypes) { + total += srs.SecretsResults.GetTotal() } - if s.IacScanResults != nil && s.IacScanResults.GetTotal() > 0 { - subScans = append(subScans, IacScan) + if srs.SastResults != nil && isFilterApply(SastResult, filterTypes) { + total += srs.SastResults.GetTotal() } - if s.ScaScanResults != nil && s.ScaScanResults.SummaryCount.GetTotal() > 0 { - subScans = append(subScans, ScaScan) + if srs.ScaResults == nil { + return } - return subScans -} - -func (svs *ScanVulnerabilitiesSummary) GetSubScanTotalIssueCount(subScanType SummarySubScanType) (count int) { - switch subScanType { - case ScaScan: - count = svs.ScaScanResults.SummaryCount.GetTotal() - case IacScan: - count = svs.IacScanResults.GetTotal() - case SecretsScan: - count = svs.SecretsScanResults.GetTotal() - case SastScan: - count = svs.SastScanResults.GetTotal() + if isFilterApply(ScaSecurityResult, filterTypes) { + total += srs.ScaResults.Security.GetTotal() + } + if isFilterApply(ScaLicenseResult, filterTypes) { + total += srs.ScaResults.License.GetTotal() + } + if isFilterApply(ScaOperationalResult, filterTypes) { + total += srs.ScaResults.OperationalRisk.GetTotal() } return } -// Severity -> Count -type SummaryCount map[string]int +func isFilterApply(key SummaryResultType, filterTypes []SummaryResultType) bool { + if len(filterTypes) == 0 { + return true + } + for _, filterType := range filterTypes { + if key == filterType { + return true + } + } + return false +} -func (sc SummaryCount) GetTotal() int { - total := 0 - for _, count := range sc { - total += count +// Returns a ResultSummary with the counts described in the summary +// Severity -> status -> Count +func (ss *ScanResultSummary) GetSummaryBySeverity() (summary ResultSummary) { + summary = ResultSummary{} + if ss.ScaResults != nil { + summary = MergeResultSummaries(summary, ss.ScaResults.Security) + summary = MergeResultSummaries(summary, ss.ScaResults.License) + summary = MergeResultSummaries(summary, ss.ScaResults.OperationalRisk) + } + if ss.IacResults != nil { + summary = MergeResultSummaries(summary, *ss.IacResults) } - return total + if ss.SecretsResults != nil { + summary = MergeResultSummaries(summary, *ss.SecretsResults) + } + if ss.SastResults != nil { + summary = MergeResultSummaries(summary, *ss.SastResults) + } + return } -// Severity -> Applicable status -> Count -type TwoLevelSummaryCount map[string]SummaryCount +func GetViolationSummaries(summaries ...ResultsSummary) *ScanViolationsSummary { + if len(summaries) == 0 { + return nil + } + violationsSummary := &ScanViolationsSummary{} + watches := datastructures.MakeSet[string]() + failBuild := false + foundViolations := false + for _, summary := range summaries { + for i := range summary.Scans { + if summary.Scans[i].Violations == nil { + continue + } + foundViolations = true + watches.AddElements(summary.Scans[i].Violations.Watches...) + failBuild = failBuild || summary.Scans[i].Violations.FailBuild + extractIssuesToSummary(&summary.Scans[i].Violations.ScanResultSummary, &violationsSummary.ScanResultSummary) + } + } + if !foundViolations { + return nil + } + violationsSummary.Watches = watches.ToSlice() + violationsSummary.FailBuild = failBuild + return violationsSummary +} -func (sc TwoLevelSummaryCount) GetTotal() (total int) { - for _, count := range sc { - total += count.GetTotal() +func GetVulnerabilitiesSummaries(summaries ...ResultsSummary) *ScanResultSummary { + if len(summaries) == 0 { + return nil } - return + vulnerabilitiesSummary := &ScanResultSummary{} + foundVulnerabilities := false + for _, summary := range summaries { + for i := range summary.Scans { + if summary.Scans[i].Vulnerabilities == nil { + continue + } + foundVulnerabilities = true + extractIssuesToSummary(summary.Scans[i].Vulnerabilities, vulnerabilitiesSummary) + } + } + if !foundVulnerabilities { + return nil + } + return vulnerabilitiesSummary } -func (sc TwoLevelSummaryCount) GetCombinedLowerLevel() (oneLvlCounts SummaryCount) { - oneLvlCounts = SummaryCount{} - for firstLvl, secondLvl := range sc { - for _, count := range secondLvl { - oneLvlCounts[firstLvl] += count +func extractIssuesToSummary(issues *ScanResultSummary, destination *ScanResultSummary) { + if issues.ScaResults != nil { + if destination.ScaResults == nil { + destination.ScaResults = &ScaScanResultSummary{} + } + destination.ScaResults.ScanIds = append(destination.ScaResults.ScanIds, issues.ScaResults.ScanIds...) + destination.ScaResults.MoreInfoUrls = append(destination.ScaResults.MoreInfoUrls, issues.ScaResults.MoreInfoUrls...) + if issues.ScaResults.Security.GetTotal() > 0 { + destination.ScaResults.Security = MergeResultSummaries(destination.ScaResults.Security, issues.ScaResults.Security) + } + if issues.ScaResults.License.GetTotal() > 0 { + destination.ScaResults.License = MergeResultSummaries(destination.ScaResults.License, issues.ScaResults.License) + } + if issues.ScaResults.OperationalRisk.GetTotal() > 0 { + destination.ScaResults.OperationalRisk = MergeResultSummaries(destination.ScaResults.OperationalRisk, issues.ScaResults.OperationalRisk) } } - return + if issues.IacResults != nil { + destination.IacResults = mergeResultSummariesPointers(destination.IacResults, issues.IacResults) + } + if issues.SecretsResults != nil { + destination.SecretsResults = mergeResultSummariesPointers(destination.SecretsResults, issues.SecretsResults) + } + if issues.SastResults != nil { + destination.SastResults = mergeResultSummariesPointers(destination.SastResults, issues.SastResults) + } } -func (sc TwoLevelSummaryCount) GetCountOfKeys(firstLevel bool) int { - if firstLevel { - return len(sc) +func mergeResultSummariesPointers(summaries ...*ResultSummary) (merged *ResultSummary) { + toMerge := []ResultSummary{} + for _, summary := range summaries { + if summary != nil { + toMerge = append(toMerge, *summary) + } } - count := datastructures.MakeSet[string]() - for _, value := range sc { - for key := range value { - count.Add(key) + result := MergeResultSummaries(toMerge...) + return &result +} + +func MergeResultSummaries(summaries ...ResultSummary) (merged ResultSummary) { + merged = ResultSummary{} + for _, summary := range summaries { + for severity, statusCount := range summary { + if _, ok := merged[severity]; !ok { + merged[severity] = statusCount + } else { + for status, count := range statusCount { + merged[severity][status] += count + } + } } } - return count.Size() + return } diff --git a/formats/summary_test.go b/formats/summary_test.go index 0e36894c..df701bed 100644 --- a/formats/summary_test.go +++ b/formats/summary_test.go @@ -1,199 +1,413 @@ package formats import ( + "sort" "testing" "github.com/stretchr/testify/assert" ) -func TestSummaryCount(t *testing.T) { +func TestCuratedPackages(t *testing.T) { testCases := []struct { - name string - count SummaryCount - expected int + name string + curatedPackages CuratedPackages + expectedApproved int + expectedBlocked int }{ - {"Empty", SummaryCount{}, 0}, - {"Single", SummaryCount{"High": 1}, 1}, - {"Multiple", SummaryCount{"High": 1, "Medium": 2, "Low": 3}, 6}, + {"Empty", CuratedPackages{}, 0, 0}, + {"Approved", CuratedPackages{PackageCount: 1}, 1, 0}, + {"Blocked", CuratedPackages{Blocked: []BlockedPackages{{Policy: "Test", Condition: "Test condition", Packages: map[string]int{"npm://test:1.0.0": 1}}}, PackageCount: 1}, 0, 1}, + { + "Multiple", + CuratedPackages{ + Blocked: []BlockedPackages{ + { + Policy: "Test", + Condition: "Test condition", + Packages: map[string]int{"npm://test:1.0.0": 1, "npm://test3:1.0.0": 1}, + }, + { + Policy: "Test2", + Condition: "Test condition 2", + Packages: map[string]int{"npm://test2:1.0.0": 1, "npm://test3:1.0.0": 1}, + }, + }, + PackageCount: 9, + }, + 6, 3, + }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - assert.Equal(t, testCase.expected, testCase.count.GetTotal()) + assert.Equal(t, testCase.expectedApproved, testCase.curatedPackages.GetApprovedCount()) + assert.Equal(t, testCase.expectedBlocked, testCase.curatedPackages.GetBlockedCount()) }) } } -func TestTwoLevelSummaryCount(t *testing.T) { +func TestResultSummary(t *testing.T) { + testSummary := ResultSummary{ + "High": map[string]int{NoStatus: 1, "Status1": 2}, + "Medium": map[string]int{"Status1": 3, "Status2": 4}, + "Low": map[string]int{NoStatus: 15}, + "Unknown": map[string]int{"Status2": 6}, + } testCases := []struct { - name string - count TwoLevelSummaryCount - expected int - expectedSeverityCountsWithoutStatus SummaryCount + name string + summary ResultSummary + severityFilters []string + expectedTotal int }{ - {"Empty", TwoLevelSummaryCount{}, 0, SummaryCount{}}, - {"Single-NoStatus", TwoLevelSummaryCount{"High": SummaryCount{"": 1}}, 1, SummaryCount{"High": 1}}, - {"Single-Status", TwoLevelSummaryCount{"High": SummaryCount{"Applicable": 1}}, 1, SummaryCount{"High": 1}}, { - "Multiple-NoStatus", - TwoLevelSummaryCount{"High": SummaryCount{"": 1}, "Medium": SummaryCount{"": 2}, "Low": SummaryCount{"": 3}}, - 6, - SummaryCount{"High": 1, "Medium": 2, "Low": 3}, + name: "Empty", + summary: ResultSummary{}, + expectedTotal: 0, }, { - "Multiple-Status", - TwoLevelSummaryCount{"High": SummaryCount{"Applicable": 1}, "Medium": SummaryCount{"": 2}, "Low": SummaryCount{"Applicable": 3, "Not Applicable": 3}}, - 9, - SummaryCount{"High": 1, "Medium": 2, "Low": 6}, + name: "No filters", + summary: testSummary, + expectedTotal: 31, + }, + { + name: "With filters", + summary: testSummary, + severityFilters: []string{"Critical", "High", "Low"}, + expectedTotal: 18, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - assert.Equal(t, testCase.expected, testCase.count.GetTotal()) - assert.Equal(t, testCase.expectedSeverityCountsWithoutStatus, testCase.count.GetCombinedLowerLevel()) + assert.Equal(t, testCase.expectedTotal, testCase.summary.GetTotal(testCase.severityFilters...)) }) } } -func TestScanVulnerabilitiesSummary(t *testing.T) { +func TestScanResultSummary(t *testing.T) { + ids := []string{"id1", "id2"} + urls := []string{"url1", "url2"} + testSummary := ScanResultSummary{ + ScaResults: &ScaScanResultSummary{ + ScanIds: ids, + MoreInfoUrls: urls, + Security: ResultSummary{"Critical": map[string]int{"Status": 1}, "High": map[string]int{NoStatus: 1}}, + License: ResultSummary{"High": map[string]int{NoStatus: 1}}, + OperationalRisk: ResultSummary{"Low": map[string]int{NoStatus: 1}}, + }, + SecretsResults: &ResultSummary{"Medium": map[string]int{NoStatus: 1}}, + SastResults: &ResultSummary{"High": map[string]int{"Status": 1}}, + } testCases := []struct { - name string - summary *ScanVulnerabilitiesSummary - expectedTotalIssueCount int - expectedTotalUniqueIssueCount int - expectedSubScansWithIssues []SummarySubScanType - expectedSubScansIssuesCount map[SummarySubScanType]int + name string + summary ScanResultSummary + resultTypeFilters []SummaryResultType + expectedTotal int + expectedScanIds []string + expectedMoreInfoUrls []string }{ { - "Empty", - &ScanVulnerabilitiesSummary{}, - 0, 0, - []SummarySubScanType{}, - map[SummarySubScanType]int{}, + name: "No Issues", + summary: ScanResultSummary{ScaResults: &ScaScanResultSummary{}, SecretsResults: &ResultSummary{}}, }, { - "Single", - &ScanVulnerabilitiesSummary{ - ScaScanResults: &ScanScaResult{ - SummaryCount: TwoLevelSummaryCount{"High": SummaryCount{"Applicable": 1}}, - UniqueFindings: 1, - }, - }, - 1, 1, - []SummarySubScanType{ScaScan}, - map[SummarySubScanType]int{ScaScan: 1}, + name: "No filters", + summary: testSummary, + expectedTotal: 6, + expectedScanIds: ids, + expectedMoreInfoUrls: urls, }, { - "Multiple", - &ScanVulnerabilitiesSummary{ - ScaScanResults: &ScanScaResult{ - SummaryCount: TwoLevelSummaryCount{"High": SummaryCount{"Applicable": 2}}, - UniqueFindings: 1, - }, - SastScanResults: &SummaryCount{"High": 1}, - }, - 3, 2, - []SummarySubScanType{SastScan, ScaScan}, - map[SummarySubScanType]int{SastScan: 1, ScaScan: 2}, + name: "One filter", + summary: testSummary, + resultTypeFilters: []SummaryResultType{ScaSecurityResult}, + expectedTotal: 2, + expectedScanIds: ids, + expectedMoreInfoUrls: urls, + }, + { + name: "Multiple filters", + summary: testSummary, + resultTypeFilters: []SummaryResultType{ScaSecurityResult, ScaLicenseResult, IacResult, SecretsResult, SastResult}, + expectedTotal: 5, + expectedScanIds: ids, + expectedMoreInfoUrls: urls, }, } + for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - validateScanVulnerabilitiesSummary(t, testCase.summary, testCase.expectedTotalIssueCount, testCase.expectedTotalUniqueIssueCount, testCase.expectedSubScansWithIssues, testCase.expectedSubScansIssuesCount) + assert.Equal(t, testCase.expectedTotal, testCase.summary.GetTotal(testCase.resultTypeFilters...)) + assert.Equal(t, testCase.expectedTotal > 0, testCase.summary.HasIssues()) + assert.ElementsMatch(t, testCase.expectedScanIds, testCase.summary.GetScanIds()) + assert.ElementsMatch(t, testCase.expectedMoreInfoUrls, testCase.summary.GetMoreInfoUrls()) }) } } -func validateScanVulnerabilitiesSummary(t *testing.T, summary *ScanVulnerabilitiesSummary, expectedTotalIssueCount, expectedTotalUniqueIssueCount int, expectedSubScansWithIssues []SummarySubScanType, expectedSubScansIssuesCount map[SummarySubScanType]int) { - assert.Equal(t, expectedTotalIssueCount, summary.GetTotalIssueCount()) - assert.Equal(t, expectedTotalUniqueIssueCount, summary.GetTotalUniqueIssueCount()) - if assert.Equal(t, expectedSubScansWithIssues, summary.GetSubScansWithIssues()) { - for subScan, expectedCount := range expectedSubScansIssuesCount { - assert.Equal(t, expectedCount, summary.GetSubScanTotalIssueCount(subScan)) - } +func TestScanSummary(t *testing.T) { + curatedBlocked := &CuratedPackages{Blocked: []BlockedPackages{{Packages: map[string]int{"npm://test:1.0.0": 1}}}, PackageCount: 1} + vulnerabilities := &ScanResultSummary{SecretsResults: &ResultSummary{"High": map[string]int{"Status": 1}}} + violations := &ScanViolationsSummary{ScanResultSummary: *vulnerabilities, Watches: []string{"watch1", "watch2"}} + testCases := []struct { + name string + summary ScanSummary + expectedHasCuratedPackages bool + expectedHasBlockedPackages bool + expectedHasViolations bool + expectedHasVulnerabilities bool + }{ + { + name: "Empty", + summary: ScanSummary{}, + }, + { + name: "CuratedPackages", + summary: ScanSummary{CuratedPackages: &CuratedPackages{PackageCount: 1}, Vulnerabilities: &ScanResultSummary{}}, + expectedHasCuratedPackages: true, + }, + { + name: "BlockedPackages", + summary: ScanSummary{CuratedPackages: curatedBlocked, Violations: &ScanViolationsSummary{}}, + expectedHasCuratedPackages: true, + expectedHasBlockedPackages: true, + }, + { + name: "Vulnerabilities", + summary: ScanSummary{Vulnerabilities: vulnerabilities}, + expectedHasVulnerabilities: true, + }, + { + name: "Violations", + summary: ScanSummary{Violations: violations}, + expectedHasViolations: true, + }, + { + name: "All", + summary: ScanSummary{CuratedPackages: curatedBlocked, Vulnerabilities: vulnerabilities, Violations: violations}, + expectedHasCuratedPackages: true, + expectedHasBlockedPackages: true, + expectedHasVulnerabilities: true, + expectedHasViolations: true, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + assert.Equal(t, testCase.expectedHasCuratedPackages, testCase.summary.HasCuratedPackages()) + assert.Equal(t, testCase.expectedHasBlockedPackages, testCase.summary.HasBlockedPackages()) + assert.Equal(t, testCase.expectedHasViolations, testCase.summary.HasViolations()) + assert.Equal(t, testCase.expectedHasVulnerabilities, testCase.summary.HasVulnerabilities()) + }) } } -func validateViolationSummary(t *testing.T, summary TwoLevelSummaryCount, expectedTotalIssueCount int, expectedViolationTypeCount map[ViolationIssueType]int) { - assert.Equal(t, expectedTotalIssueCount, summary.GetTotal()) - for violationType, expectedCount := range expectedViolationTypeCount { - assert.Equal(t, expectedCount, summary[violationType.String()].GetTotal()) +func TestResultsSummary(t *testing.T) { + testScans := []ScanSummary{ + {Vulnerabilities: &ScanResultSummary{SecretsResults: &ResultSummary{"High": map[string]int{"Status": 4}}}}, + {Violations: &ScanViolationsSummary{ScanResultSummary: ScanResultSummary{ScaResults: &ScaScanResultSummary{License: ResultSummary{"Medium": map[string]int{NoStatus: 2}}}, SastResults: &ResultSummary{"High": map[string]int{"Status": 1}}}}}, + {Vulnerabilities: &ScanResultSummary{SastResults: &ResultSummary{"Medium": map[string]int{NoStatus: 1}}}}, + {Vulnerabilities: &ScanResultSummary{ScaResults: &ScaScanResultSummary{Security: ResultSummary{"Critical": map[string]int{"Status": 3}}}}}, + {Vulnerabilities: &ScanResultSummary{ScaResults: &ScaScanResultSummary{Security: ResultSummary{"High": map[string]int{NoStatus: 1}, "Low": map[string]int{NoStatus: 1}}}}}, + } + testCases := []struct { + name string + summary ResultsSummary + filters []SummaryResultType + expectedTotalVulnerabilities int + expectedTotalViolations int + }{ + { + name: "Empty", + summary: ResultsSummary{}, + }, + { + name: "No filters", + summary: ResultsSummary{Scans: testScans}, + expectedTotalVulnerabilities: 10, + expectedTotalViolations: 3, + }, + { + name: "With filters", + summary: ResultsSummary{Scans: testScans}, + filters: []SummaryResultType{ScaLicenseResult, IacResult, SecretsResult}, + expectedTotalVulnerabilities: 4, + expectedTotalViolations: 2, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + assert.Equal(t, testCase.expectedTotalVulnerabilities, testCase.summary.GetTotalVulnerabilities(testCase.filters...)) + assert.Equal(t, testCase.expectedTotalViolations, testCase.summary.GetTotalViolations(testCase.filters...)) + assert.Equal(t, testCase.expectedTotalViolations > 0, testCase.summary.HasViolations()) + }) } } -func TestScanSummaryResult(t *testing.T) { +func TestGetVulnerabilitiesSummaries(t *testing.T) { + dummyScaResults := &ScaScanResultSummary{Security: ResultSummary{"High": map[string]int{NoStatus: 1}}} + dummyResultSummary := &ResultSummary{"Medium": map[string]int{NoStatus: 1}} testCases := []struct { - name string - result *ScanSummaryResult - - expectedTotalIssueCount int - expectedTotalVulnerabilityCount int - expectedTotalViolationCount int + name string + input []ResultsSummary + expectedShowVulnerabilities bool + expectedVulnerabilitiesSummaries *ScanResultSummary + }{ + { + name: "Vulnerabilities not requested", + input: []ResultsSummary{}, + }, + { + name: "No Vulnerabilities", + expectedShowVulnerabilities: true, + input: []ResultsSummary{{Scans: []ScanSummary{{Target: "target", Vulnerabilities: &ScanResultSummary{}}}}}, + expectedVulnerabilitiesSummaries: &ScanResultSummary{}, + }, + { + name: "Single input", + expectedShowVulnerabilities: true, + input: []ResultsSummary{{Scans: []ScanSummary{{Target: "target", Vulnerabilities: &ScanResultSummary{ScaResults: dummyScaResults, SecretsResults: dummyResultSummary}}}}}, + expectedVulnerabilitiesSummaries: &ScanResultSummary{ScaResults: dummyScaResults, SecretsResults: dummyResultSummary}, + }, + { + name: "Multiple inputs", + expectedShowVulnerabilities: true, + input: []ResultsSummary{ + {Scans: []ScanSummary{{Target: "target1", Vulnerabilities: &ScanResultSummary{ScaResults: dummyScaResults}}}}, + { + Scans: []ScanSummary{ + {Target: "target2", Vulnerabilities: &ScanResultSummary{SecretsResults: dummyResultSummary}}, + {Target: "target3", Vulnerabilities: &ScanResultSummary{SastResults: dummyResultSummary}}, + }, + }, + }, + expectedVulnerabilitiesSummaries: &ScanResultSummary{ScaResults: dummyScaResults, SecretsResults: dummyResultSummary, SastResults: dummyResultSummary}, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + vulnerabilitiesSummaries := GetVulnerabilitiesSummaries(testCase.input...) + if testCase.expectedShowVulnerabilities { + assert.Equal(t, testCase.expectedVulnerabilitiesSummaries, vulnerabilitiesSummaries) + } else { + assert.Nil(t, vulnerabilitiesSummaries) + } + }) + } +} - expectedSubScansWithIssues []SummarySubScanType - expectedSubScansIssuesCount map[SummarySubScanType]int - expectedViolationTypeCount map[ViolationIssueType]int +func TestGetViolationSummaries(t *testing.T) { + testCases := []struct { + name string + input []ResultsSummary + expectedShowViolations bool + expectedViolationSummaries *ScanViolationsSummary }{ { - "Empty", - &ScanSummaryResult{}, - 0, 0, 0, - []SummarySubScanType{}, - map[SummarySubScanType]int{}, - map[ViolationIssueType]int{}, + name: "violation context not defined", + input: []ResultsSummary{}, }, { - "Single", - &ScanSummaryResult{ - Vulnerabilities: &ScanVulnerabilitiesSummary{ - ScaScanResults: &ScanScaResult{ - SummaryCount: TwoLevelSummaryCount{"High": SummaryCount{"Applicable": 1}}, - UniqueFindings: 1, + name: "No Violations", + expectedShowViolations: true, + input: []ResultsSummary{{Scans: []ScanSummary{{Target: "target", Violations: &ScanViolationsSummary{}}}}}, + expectedViolationSummaries: &ScanViolationsSummary{}, + }, + { + name: "Single input", + expectedShowViolations: true, + input: []ResultsSummary{{Scans: []ScanSummary{ + { + Target: "target1", + Violations: &ScanViolationsSummary{ + Watches: []string{"watch1"}, + FailBuild: true, + ScanResultSummary: ScanResultSummary{ScaResults: &ScaScanResultSummary{ + Security: ResultSummary{"Critical": map[string]int{NoStatus: 1}}, + OperationalRisk: ResultSummary{"Low": map[string]int{NoStatus: 1}}, + }}, }, }, + { + Target: "target2", + Violations: &ScanViolationsSummary{ + Watches: []string{"watch2"}, + ScanResultSummary: ScanResultSummary{ScaResults: &ScaScanResultSummary{ + Security: ResultSummary{"High": map[string]int{NoStatus: 1}}, + License: ResultSummary{"High": map[string]int{NoStatus: 1}}, + }}, + }, + }, + }}}, + expectedViolationSummaries: &ScanViolationsSummary{ + Watches: []string{"watch1", "watch2"}, + FailBuild: true, + ScanResultSummary: ScanResultSummary{ScaResults: &ScaScanResultSummary{ + Security: ResultSummary{"Critical": map[string]int{NoStatus: 1}, "High": map[string]int{NoStatus: 1}}, + License: ResultSummary{"High": map[string]int{NoStatus: 1}}, + OperationalRisk: ResultSummary{"Low": map[string]int{NoStatus: 1}}, + }}, }, - 1, 1, 0, - []SummarySubScanType{ScaScan}, - map[SummarySubScanType]int{ScaScan: 1}, - map[ViolationIssueType]int{}, }, { - "Multiple", - &ScanSummaryResult{ - Vulnerabilities: &ScanVulnerabilitiesSummary{ - ScaScanResults: &ScanScaResult{ - SummaryCount: TwoLevelSummaryCount{"High": SummaryCount{"Applicable": 1}}, - UniqueFindings: 1, + name: "Multiple inputs", + expectedShowViolations: true, + input: []ResultsSummary{ + { + Scans: []ScanSummary{ + { + Target: "target1", + Violations: &ScanViolationsSummary{ + Watches: []string{"watch1"}, + ScanResultSummary: ScanResultSummary{ScaResults: &ScaScanResultSummary{ + Security: ResultSummary{"Critical": map[string]int{NoStatus: 1}}, + OperationalRisk: ResultSummary{"Low": map[string]int{NoStatus: 1}}, + }}, + }, + }, + { + Target: "target2", + Violations: &ScanViolationsSummary{ + Watches: []string{"watch2"}, + ScanResultSummary: ScanResultSummary{ScaResults: &ScaScanResultSummary{ + Security: ResultSummary{"High": map[string]int{NoStatus: 1}}, + License: ResultSummary{"High": map[string]int{NoStatus: 1}}, + }}, + }, + }, }, - SastScanResults: &SummaryCount{"High": 1}, }, - Violations: TwoLevelSummaryCount{ - ViolationTypeSecurity.String(): {"High": 1}, - ViolationTypeLicense.String(): {"High": 1}, - ViolationTypeOperationalRisk.String(): {"High": 1}, + { + Scans: []ScanSummary{ + { + Target: "target3", + Violations: &ScanViolationsSummary{ + Watches: []string{"watch1"}, + ScanResultSummary: ScanResultSummary{ScaResults: &ScaScanResultSummary{ + Security: ResultSummary{"Critical": map[string]int{NoStatus: 1, "status": 2}, "High": map[string]int{NoStatus: 1}}, + License: ResultSummary{"Medium": map[string]int{NoStatus: 1}}, + }}, + }, + }, + }, }, }, - 5, 2, 3, - []SummarySubScanType{SastScan, ScaScan}, - map[SummarySubScanType]int{SastScan: 1, ScaScan: 1}, - map[ViolationIssueType]int{ViolationTypeSecurity: 1, ViolationTypeLicense: 1, ViolationTypeOperationalRisk: 1}, + expectedViolationSummaries: &ScanViolationsSummary{ + Watches: []string{"watch1", "watch2"}, + ScanResultSummary: ScanResultSummary{ScaResults: &ScaScanResultSummary{ + Security: ResultSummary{"Critical": map[string]int{NoStatus: 2, "status": 2}, "High": map[string]int{NoStatus: 2}}, + License: ResultSummary{"High": map[string]int{NoStatus: 1}, "Medium": map[string]int{NoStatus: 1}}, + OperationalRisk: ResultSummary{"Low": map[string]int{NoStatus: 1}}, + }}, + }, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - // validate general - assert.Equal(t, testCase.expectedTotalIssueCount > 0, testCase.result.HasIssues()) - assert.Equal(t, testCase.expectedTotalIssueCount, testCase.result.GetTotalIssueCount()) - assert.Equal(t, testCase.expectedTotalViolationCount > 0, testCase.result.HasViolations()) - assert.Equal(t, testCase.expectedTotalViolationCount, testCase.result.GetTotalViolationCount()) - - assert.Equal(t, testCase.expectedTotalVulnerabilityCount > 0, testCase.result.HasSecurityVulnerabilities()) - - // validate content - if testCase.result.Vulnerabilities != nil { - validateScanVulnerabilitiesSummary(t, testCase.result.Vulnerabilities, testCase.expectedTotalVulnerabilityCount, testCase.expectedTotalVulnerabilityCount, testCase.expectedSubScansWithIssues, testCase.expectedSubScansIssuesCount) + violationSummaries := GetViolationSummaries(testCase.input...) + if testCase.expectedShowViolations { + sort.Strings(violationSummaries.Watches) + assert.Equal(t, testCase.expectedViolationSummaries, violationSummaries) + } else { + assert.Nil(t, violationSummaries) } - validateViolationSummary(t, testCase.result.Violations, testCase.expectedTotalViolationCount, testCase.expectedViolationTypeCount) }) } - } diff --git a/go.mod b/go.mod index ddd0a3e7..42caa94e 100644 --- a/go.mod +++ b/go.mod @@ -6,17 +6,17 @@ require ( github.com/beevik/etree v1.4.0 github.com/google/go-github/v56 v56.0.0 github.com/gookit/color v1.5.4 - github.com/jfrog/build-info-go v1.9.34 + github.com/jfrog/build-info-go v1.9.35 github.com/jfrog/froggit-go v1.16.1 github.com/jfrog/gofrog v1.7.5 github.com/jfrog/jfrog-apps-config v1.0.1 - github.com/jfrog/jfrog-cli-core/v2 v2.55.2 - github.com/jfrog/jfrog-client-go v1.44.2 + github.com/jfrog/jfrog-cli-core/v2 v2.55.6 + github.com/jfrog/jfrog-client-go v1.46.1 github.com/magiconair/properties v1.8.7 github.com/owenrumney/go-sarif/v2 v2.3.0 github.com/stretchr/testify v1.9.0 github.com/urfave/cli v1.22.15 - golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa + golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 golang.org/x/sync v0.8.0 golang.org/x/text v0.17.0 gopkg.in/yaml.v3 v3.0.1 @@ -93,7 +93,7 @@ require ( github.com/spf13/viper v1.19.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/ulikunitz/xz v0.5.12 // indirect - github.com/vbauerster/mpb/v8 v8.7.5 // indirect + github.com/vbauerster/mpb/v8 v8.8.3 // indirect github.com/xanzy/go-gitlab v0.95.2 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect @@ -104,7 +104,7 @@ require ( golang.org/x/mod v0.20.0 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/oauth2 v0.18.0 // indirect - golang.org/x/sys v0.23.0 // indirect + golang.org/x/sys v0.24.0 // indirect golang.org/x/term v0.23.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.24.0 // indirect diff --git a/go.sum b/go.sum index 77d8088e..854d47fa 100644 --- a/go.sum +++ b/go.sum @@ -890,18 +890,18 @@ github.com/jedib0t/go-pretty/v6 v6.5.9 h1:ACteMBRrrmm1gMsXe9PSTOClQ63IXDUt03H5U+ github.com/jedib0t/go-pretty/v6 v6.5.9/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E= github.com/jfrog/archiver/v3 v3.6.1 h1:LOxnkw9pOn45DzCbZNFV6K0+6dCsQ0L8mR3ZcujO5eI= github.com/jfrog/archiver/v3 v3.6.1/go.mod h1:VgR+3WZS4N+i9FaDwLZbq+jeU4B4zctXL+gL4EMzfLw= -github.com/jfrog/build-info-go v1.9.34 h1:bPnW58VpclbpBe/x8XEu/2BIviEOoJrJ5PkRRcmU3Co= -github.com/jfrog/build-info-go v1.9.34/go.mod h1:6mdtqjREK76bHNODXakqKR/+ksJ9dvfLS7H57BZtnLY= +github.com/jfrog/build-info-go v1.9.35 h1:P53Ckbuin0GYrq0LWMY0GZSptJcQwiUyW6lqTbXKdcc= +github.com/jfrog/build-info-go v1.9.35/go.mod h1:6mdtqjREK76bHNODXakqKR/+ksJ9dvfLS7H57BZtnLY= github.com/jfrog/froggit-go v1.16.1 h1:FBIM1qevX/ag9unfmpGzfmZ36D8ulOJ+DPTSFUk3l5U= github.com/jfrog/froggit-go v1.16.1/go.mod h1:TEJSzgiV+3D/GVGE8Y6j46ut1jrBLD1FL6WdMdKwwCE= github.com/jfrog/gofrog v1.7.5 h1:dFgtEDefJdlq9cqTRoe09RLxS5Bxbe1Ev5+E6SmZHcg= github.com/jfrog/gofrog v1.7.5/go.mod h1:jyGiCgiqSSR7k86hcUSu67XVvmvkkgWTmPsH25wI298= github.com/jfrog/jfrog-apps-config v1.0.1 h1:mtv6k7g8A8BVhlHGlSveapqf4mJfonwvXYLipdsOFMY= github.com/jfrog/jfrog-apps-config v1.0.1/go.mod h1:8AIIr1oY9JuH5dylz2S6f8Ym2MaadPLR6noCBO4C22w= -github.com/jfrog/jfrog-cli-core/v2 v2.55.2 h1:Pm4mY1UThSyFGklDl6O8qoJgTgH9jL3i2tor/ux+X8c= -github.com/jfrog/jfrog-cli-core/v2 v2.55.2/go.mod h1:2/Ccqq0ayMqIuH5AAoneX0CowwdrNWQcs5aKz8iDYkE= -github.com/jfrog/jfrog-client-go v1.44.2 h1:5t8tx6NOth6Xq24SdF3MYSd6vo0bTibW93nads2DEuY= -github.com/jfrog/jfrog-client-go v1.44.2/go.mod h1:f5Jfv+RGKVr4smOp4a4pxyBKdlpLG7R894kx2XW+w8c= +github.com/jfrog/jfrog-cli-core/v2 v2.55.6 h1:3tQuEdYgS2q7fkrrSG66OnO0S998FXGaY9BVsxSLst4= +github.com/jfrog/jfrog-cli-core/v2 v2.55.6/go.mod h1:DPO5BfWAeOByahFMMy+PcjmbPlcyoRy7Bf2C5sGKVi0= +github.com/jfrog/jfrog-client-go v1.46.1 h1:ExqOF8ClOG9LO3vbm6jTIwQHHhprbu8lxB2RrM6mMI0= +github.com/jfrog/jfrog-client-go v1.46.1/go.mod h1:UCu2JNBfMp9rypEmCL84DCooG79xWIHVadZQR3Ab+BQ= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= @@ -1064,8 +1064,8 @@ github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/cli v1.22.15 h1:nuqt+pdC/KqswQKhETJjo7pvn/k4xMUxgW6liI7XpnM= github.com/urfave/cli v1.22.15/go.mod h1:wSan1hmo5zeyLGBjRJbzRTNk8gwoYa2B9n4q9dmRIc0= -github.com/vbauerster/mpb/v8 v8.7.5 h1:hUF3zaNsuaBBwzEFoCvfuX3cpesQXZC0Phm/JcHZQ+c= -github.com/vbauerster/mpb/v8 v8.7.5/go.mod h1:bRCnR7K+mj5WXKsy0NWB6Or+wctYGvVwKn6huwvxKa0= +github.com/vbauerster/mpb/v8 v8.8.3 h1:dTOByGoqwaTJYPubhVz3lO5O6MK553XVgUo33LdnNsQ= +github.com/vbauerster/mpb/v8 v8.8.3/go.mod h1:JfCCrtcMsJwP6ZwMn9e5LMnNyp3TVNpUWWkN+nd4EWk= github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/xanzy/go-gitlab v0.95.2 h1:4p0IirHqEp5f0baK/aQqr4TR57IsD+8e4fuyAA1yi88= @@ -1140,8 +1140,8 @@ golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI= -golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= +golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA= +golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -1392,8 +1392,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= -golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/resources/resources.go b/resources/resources.go new file mode 100644 index 00000000..d1e5931a --- /dev/null +++ b/resources/resources.go @@ -0,0 +1,3 @@ +package resources + +const BaseResourcesUrl = "https://raw.githubusercontent.com/jfrog/jfrog-cli-security/main/resources" diff --git a/resources/statusIcons/failed.svg b/resources/statusIcons/failed.svg new file mode 100644 index 00000000..bbd5646a --- /dev/null +++ b/resources/statusIcons/failed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/statusIcons/passed.svg b/resources/statusIcons/passed.svg new file mode 100644 index 00000000..34a8778c --- /dev/null +++ b/resources/statusIcons/passed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/testdata/other/jobSummary/binary_vulnerabilities.md b/tests/testdata/other/jobSummary/binary_vulnerabilities.md new file mode 100644 index 00000000..2e2b6762 --- /dev/null +++ b/tests/testdata/other/jobSummary/binary_vulnerabilities.md @@ -0,0 +1 @@ +
44 Security issues are grouped by CVE number:	44 SCA\ No newline at end of file diff --git a/tests/testdata/other/jobSummary/build_scan_vulnerabilities.md b/tests/testdata/other/jobSummary/build_scan_vulnerabilities.md new file mode 100644 index 00000000..ae008afc --- /dev/null +++ b/tests/testdata/other/jobSummary/build_scan_vulnerabilities.md @@ -0,0 +1 @@ +āļø 33 Criticalš” 11 Low
See the results of the scan in JFrog
24 Security Issues:	24 SCA\ No newline at end of file diff --git a/tests/testdata/other/jobSummary/docker_vulnerabilities.md b/tests/testdata/other/jobSummary/docker_vulnerabilities.md new file mode 100644 index 00000000..399269f7 --- /dev/null +++ b/tests/testdata/other/jobSummary/docker_vulnerabilities.md @@ -0,0 +1 @@ +š“ 3 Highš 1 MediumāŖļø 20 Unknown
See the results of the scan in JFrog
20 Security issues are grouped by CVE number:	17 SCA	3 Secrets\ No newline at end of file diff --git a/tests/testdata/other/jobSummary/multi_command_job.md b/tests/testdata/other/jobSummary/multi_command_job.md deleted file mode 100644 index f383ea6b..00000000 --- a/tests/testdata/other/jobSummary/multi_command_job.md +++ /dev/null @@ -1,21 +0,0 @@ -#### Builds -| Status | Id | Details | -|--------|----|---------| -| ā | build-name (build-number) | | -| ā | build-name (build-number) |āļø 8 Critical (2 Not Applicable)š“ 5 Highš 3 Mediumš” 3 Low (3 Not Applicable)āŖļø 1 Unknown
See the results of the scan in JFrog
Violations: 4 - (2 Security, 1 License, 1 Operational)| -#### Artifacts -| Status | Id | Details | -|--------|----|---------| -| ā | /binary-name |
Security Vulnerabilities: 3 (3 unique)| -| ā | other-root/dir/binary-name2 | | -#### Modules -| Status | Id | Details | -|--------|----|---------| -| ā | /application1 |
āāā 3 Secrets š“ 2 High
š” 1 Low
Security Vulnerabilities: 14 (12 unique)| -| ā | /application2 |
āāā 1 SAST š” 1 Low
āāā 5 IAC š 5 Medium
āāā 8 SCA āļø 3 Critical (2 Not Applicable)
š“ 4 High (1 Applicable, 1 Not Applicable)
š” 1 Low
Violations: 1 - (1 Security)| -| ā | /dir/application3 | | -#### Curation -| Status | Id | Details | -|--------|----|---------| -| ā | /application1 |
Security Vulnerabilities: 1 (1 unique)
āāā 1 SCA š“ 1 High (1 Not Applicable)
Total Number of Packages: 6| -| ā | /application2 |
š¢ Total Number of Approved Packages: 4
š“ Total Number of Blocked Packages: 2
āāā Policy: cvss_score, Condition:cvss score higher than 4.0 (1)
āāā Policy: Malicious, Condition: Malicious package (1)
Total Number of Packages: 6| \ No newline at end of file diff --git a/tests/testdata/other/jobSummary/no_violations.md b/tests/testdata/other/jobSummary/no_violations.md new file mode 100644 index 00000000..89f2b3bc --- /dev/null +++ b/tests/testdata/other/jobSummary/no_violations.md @@ -0,0 +1 @@ +
š¢ Total Number of Approved Packages: 4
š“ Total Number of Blocked Packages: 2
āāā Policy: License, Condition: GPL (1)
āāā Policy: Aged, Condition: Package is aged (1)
No violations found\ No newline at end of file diff --git a/tests/testdata/other/jobSummary/no_vulnerabilities.md b/tests/testdata/other/jobSummary/no_vulnerabilities.md new file mode 100644 index 00000000..e724deff --- /dev/null +++ b/tests/testdata/other/jobSummary/no_vulnerabilities.md @@ -0,0 +1 @@ +
No security issues found\ No newline at end of file diff --git a/tests/testdata/other/jobSummary/security_section.md b/tests/testdata/other/jobSummary/security_section.md new file mode 100644 index 00000000..517122ca --- /dev/null +++ b/tests/testdata/other/jobSummary/security_section.md @@ -0,0 +1,8 @@ +
Total Number of resolved packages: 6| +| | /application2 |
š¢ Approved packages: 3
š“ Blocked packages: 3Violated Policy: cvss_score, Condition: cvss score higher than 4.0 (2)
š¦ npm://test:2.0.0
š¦ npm://underscore:1.0.0Violated Policy: Malicious, Condition: Malicious package (1)
š¦ npm://lodash:1.0.0
Total Number of resolved packages: 3| +| | /application3 |
Total Number of resolved packages: 5|
š¢ Approved packages: 4
š“ Blocked packages: 1Violated Policy: Aged, Condition: Package is aged (1)
š¦ npm://test:1.0.0
Violations: 1 - (1 License)\ No newline at end of file diff --git a/tests/testdata/other/jobSummary/single_no_issue.md b/tests/testdata/other/jobSummary/single_no_issue.md deleted file mode 100644 index 1a19cc7a..00000000 --- a/tests/testdata/other/jobSummary/single_no_issue.md +++ /dev/null @@ -1,3 +0,0 @@ -``` -ā No issues were found -``` \ No newline at end of file diff --git a/tests/testdata/other/jobSummary/violations.md b/tests/testdata/other/jobSummary/violations.md new file mode 100644 index 00000000..f604e7e6 --- /dev/null +++ b/tests/testdata/other/jobSummary/violations.md @@ -0,0 +1 @@ +
Security Vulnerabilities: 3 (3 unique)
āāā 3 Secrets š“ 2 High
š” 1 Low
watches:
watch1, watch2, watch3, watch4
watch5
23 Policy Violations:	17 Security	2 Operational	1 License	3 Secrets\ No newline at end of file diff --git a/tests/testdata/other/jobSummary/violations_not_defined.md b/tests/testdata/other/jobSummary/violations_not_defined.md new file mode 100644 index 00000000..dd8c130d --- /dev/null +++ b/tests/testdata/other/jobSummary/violations_not_defined.md @@ -0,0 +1 @@ +āļø 8 Critical (2 Not Applicable)š“ 6 Highš 3 Mediumš” 5 Low (3 Not Applicable)āŖļø 1 Unknown
See the results of the scan in JFrog
No watch is defined\ No newline at end of file diff --git a/tests/testdata/other/jobSummary/violations_not_extended_view.md b/tests/testdata/other/jobSummary/violations_not_extended_view.md new file mode 100644 index 00000000..0702399a --- /dev/null +++ b/tests/testdata/other/jobSummary/violations_not_extended_view.md @@ -0,0 +1 @@ +
watch: watch1
26 Policy Violations:	20 Security	2 Operational	1 License	3 Secrets\ No newline at end of file diff --git a/utils/results.go b/utils/results.go index dd51c053..b1404099 100644 --- a/utils/results.go +++ b/utils/results.go @@ -81,41 +81,12 @@ func (r *Results) IsIssuesFound() bool { // Counts the total number of unique findings in the provided results. // A unique SCA finding is identified by a unique pair of vulnerability's/violation's issueId and component id or by a result returned from one of JAS scans. -func (r *Results) CountScanResultsFindings() (total int) { - return formats.SummaryResults{Scans: r.getScanSummaryByTargets()}.GetTotalIssueCount() -} -func (r *Results) GetSummary() (summary formats.SummaryResults) { - if len(r.ScaResults) <= 1 { - summary.Scans = r.getScanSummaryByTargets() - return - } - for _, scaScan := range r.ScaResults { - summary.Scans = append(summary.Scans, r.getScanSummaryByTargets(scaScan.Target)...) - } - return -} - -// Returns a summary for the provided targets. If no targets are provided, a summary for all targets is returned. -func (r *Results) getScanSummaryByTargets(targets ...string) (summaries []formats.ScanSummaryResult) { - if len(targets) == 0 { - // No filter, one scan summary for all targets - summaries = append(summaries, getScanSummary(r.ExtendedScanResults, r.ScaResults...)) - return +func (r *Results) CountScanResultsFindings(includeVulnerabilities, includeViolations bool) (total int) { + summary := formats.ResultsSummary{Scans: GetScanSummaryByTargets(r, includeVulnerabilities, includeViolations)} + if summary.HasViolations() { + return summary.GetTotalViolations() } - for _, target := range targets { - // Get target sca results - targetScaResults := []*ScaScanResult{} - if targetScaResult := r.getScaScanResultByTarget(target); targetScaResult != nil { - targetScaResults = append(targetScaResults, targetScaResult) - } - // Get target extended results - targetExtendedResults := r.ExtendedScanResults - if targetExtendedResults != nil { - targetExtendedResults = targetExtendedResults.GetResultsForTarget(target) - } - summaries = append(summaries, getScanSummary(targetExtendedResults, targetScaResults...)) - } - return + return summary.GetTotalVulnerabilities() } type ScaScanResult struct { diff --git a/utils/results_test.go b/utils/results_test.go index 6ed05739..c85e7132 100644 --- a/utils/results_test.go +++ b/utils/results_test.go @@ -1,13 +1,8 @@ package utils import ( - "testing" - - "github.com/jfrog/jfrog-cli-security/formats" - "github.com/jfrog/jfrog-cli-security/formats/sarifutils" - "github.com/jfrog/jfrog-client-go/xray/services" - "github.com/owenrumney/go-sarif/v2/sarif" "github.com/stretchr/testify/assert" + "testing" ) func TestGetScaScanResultByTarget(t *testing.T) { @@ -49,155 +44,3 @@ func TestGetScaScanResultByTarget(t *testing.T) { }) } } - -func TestGetSummary(t *testing.T) { - dummyExtendedScanResults := &ExtendedScanResults{ - ApplicabilityScanResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResults(sarifutils.CreateDummyPassingResult("applic_CVE-2")).WithInvocations([]*sarif.Invocation{ - sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation("target1")), - }), - }, - SecretsScanResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResults(sarifutils.CreateResultWithLocations("", "", "note", sarifutils.CreateLocation("target1/file", 0, 0, 0, 0, "snippet"))).WithInvocations([]*sarif.Invocation{ - sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation("target1")), - }), - sarifutils.CreateRunWithDummyResults(sarifutils.CreateResultWithLocations("", "", "note", sarifutils.CreateLocation("target2/file", 0, 0, 0, 0, "snippet"))).WithInvocations([]*sarif.Invocation{ - sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation("target2")), - }), - }, - SastScanResults: []*sarif.Run{ - sarifutils.CreateRunWithDummyResults(sarifutils.CreateResultWithLocations("", "", "note", sarifutils.CreateLocation("target1/file2", 0, 0, 0, 0, "snippet"))).WithInvocations([]*sarif.Invocation{ - sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation("target1")), - }), - }, - } - - testCases := []struct { - name string - results Results - expected formats.SummaryResults - findingCount int - issueCount int - }{ - { - name: "Empty results", - results: Results{ScaResults: []*ScaScanResult{}}, - expected: formats.SummaryResults{Scans: []formats.ScanSummaryResult{{}}}, - findingCount: 0, - issueCount: 0, - }, - { - name: "One module result", - results: Results{ - ScaResults: []*ScaScanResult{{ - Target: "target1", - XrayResults: getDummyScaTestResults(true, false), - }}, - ExtendedScanResults: dummyExtendedScanResults, - }, - expected: formats.SummaryResults{ - Scans: []formats.ScanSummaryResult{ - { - Target: "target1", - Vulnerabilities: &formats.ScanVulnerabilitiesSummary{ - ScaScanResults: &formats.ScanScaResult{ - SummaryCount: formats.TwoLevelSummaryCount{ - "Critical": formats.SummaryCount{"Undetermined": 1}, - "High": formats.SummaryCount{"Not Applicable": 1}, - }, - UniqueFindings: 2, - }, - SecretsScanResults: &formats.SummaryCount{"Low": 2}, - SastScanResults: &formats.SummaryCount{"Low": 1}, - }, - Violations: formats.TwoLevelSummaryCount{}, - }, - }, - }, - findingCount: 5, - issueCount: 5, - }, - { - name: "Multiple module results", - results: Results{ - ScaResults: []*ScaScanResult{ - { - Target: "target1", - XrayResults: getDummyScaTestResults(false, true), - }, - { - Target: "target2", - XrayResults: getDummyScaTestResults(true, true), - }, - }, - ExtendedScanResults: dummyExtendedScanResults, - }, - expected: formats.SummaryResults{ - Scans: []formats.ScanSummaryResult{ - { - Target: "target1", - Vulnerabilities: &formats.ScanVulnerabilitiesSummary{ - ScaScanResults: &formats.ScanScaResult{SummaryCount: formats.TwoLevelSummaryCount{}}, - SecretsScanResults: &formats.SummaryCount{"Low": 1}, - SastScanResults: &formats.SummaryCount{"Low": 1}, - }, - Violations: formats.TwoLevelSummaryCount{ - formats.ViolationTypeSecurity.String(): formats.SummaryCount{"Critical": 1, "High": 1}, - formats.ViolationTypeLicense.String(): formats.SummaryCount{"High": 1}, - }, - }, - { - Target: "target2", - Vulnerabilities: &formats.ScanVulnerabilitiesSummary{ - ScaScanResults: &formats.ScanScaResult{ - SummaryCount: formats.TwoLevelSummaryCount{"Critical": formats.SummaryCount{"": 1}}, - UniqueFindings: 1, - }, - SecretsScanResults: &formats.SummaryCount{"Low": 1}, - }, - Violations: formats.TwoLevelSummaryCount{formats.ViolationTypeSecurity.String(): formats.SummaryCount{"High": 1}}, - }, - }, - }, - findingCount: 7, - issueCount: 8, - }, - } - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - result := testCase.results.GetSummary() - assert.Equal(t, testCase.expected, result) - assert.Equal(t, testCase.findingCount, testCase.results.CountScanResultsFindings()) - assert.Equal(t, testCase.issueCount, testCase.results.GetSummary().GetTotalIssueCount()) - }) - } -} - -func getDummyScaTestResults(vulnerability, violation bool) (responses []services.ScanResponse) { - response := services.ScanResponse{} - switch { - case vulnerability && violation: - // Mix - response.Vulnerabilities = []services.Vulnerability{ - {IssueId: "XRAY-1", Severity: "Critical", Cves: []services.Cve{{Id: "CVE-1"}}, Components: map[string]services.Component{"issueId_direct_dependency": {}}}, - } - response.Violations = []services.Violation{ - {ViolationType: formats.ViolationTypeSecurity.String(), WatchName: "test-watch-name", IssueId: "XRAY-2", Severity: "High", Cves: []services.Cve{{Id: "CVE-2"}}, Components: map[string]services.Component{"issueId_direct_dependency": {}}}, - } - case vulnerability: - // only vulnerability - response.Vulnerabilities = []services.Vulnerability{ - {IssueId: "XRAY-1", Severity: "Critical", Cves: []services.Cve{{Id: "CVE-1"}}, Components: map[string]services.Component{"issueId_direct_dependency": {}}}, - {IssueId: "XRAY-2", Severity: "High", Cves: []services.Cve{{Id: "CVE-2"}}, Components: map[string]services.Component{"issueId_direct_dependency": {}}}, - } - case violation: - // only violation - response.Violations = []services.Violation{ - {ViolationType: formats.ViolationTypeSecurity.String(), WatchName: "test-watch-name", IssueId: "XRAY-1", Severity: "Critical", Cves: []services.Cve{{Id: "CVE-1"}}, Components: map[string]services.Component{"issueId_direct_dependency": {}}}, - {ViolationType: formats.ViolationTypeSecurity.String(), WatchName: "test-watch-name", IssueId: "XRAY-2", Severity: "High", Cves: []services.Cve{{Id: "CVE-2"}}, Components: map[string]services.Component{"issueId_direct_dependency": {}}}, - {ViolationType: formats.ViolationTypeLicense.String(), WatchName: "test-watch-name", IssueId: "MIT", Severity: "High", LicenseKey: "MIT", Components: map[string]services.Component{"issueId_direct_dependency": {}}}, - } - } - responses = append(responses, response) - return -} diff --git a/utils/resultstable.go b/utils/resultstable.go index 1ad6daae..bf87b948 100644 --- a/utils/resultstable.go +++ b/utils/resultstable.go @@ -90,7 +90,7 @@ func prepareViolations(violations []services.Violation, results *Results, multip return nil, nil, nil, err } switch violation.ViolationType { - case formats.ViolationTypeSecurity.String(): + case ViolationTypeSecurity.String(): cves := convertCves(violation.Cves) if results.ExtendedScanResults.EntitledForJas { for i := range cves { @@ -125,7 +125,7 @@ func prepareViolations(violations []services.Violation, results *Results, multip }, ) } - case formats.ViolationTypeLicense.String(): + case ViolationTypeLicense.String(): currSeverity, err := severityutils.ParseSeverity(violation.Severity, false) if err != nil { return nil, nil, nil, err @@ -144,7 +144,7 @@ func prepareViolations(violations []services.Violation, results *Results, multip }, ) } - case formats.ViolationTypeOperationalRisk.String(): + case ViolationTypeOperationalRisk.String(): currSeverity, err := severityutils.ParseSeverity(violation.Severity, false) if err != nil { return nil, nil, nil, err diff --git a/utils/resultwriter.go b/utils/resultwriter.go index dc68d2bb..53a21d62 100644 --- a/utils/resultwriter.go +++ b/utils/resultwriter.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" + "github.com/jfrog/gofrog/datastructures" "github.com/jfrog/jfrog-cli-core/v2/common/format" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-security/formats" @@ -606,3 +607,200 @@ func IsEmptyScanResponse(results []services.ScanResponse) bool { func NewFailBuildError() error { return coreutils.CliError{ExitCode: coreutils.ExitCodeVulnerableBuild, ErrorMsg: "One or more of the violations found are set to fail builds that include them"} } + +func ToSummary(cmdResult *Results, includeVulnerabilities, includeViolations bool) (summary formats.ResultsSummary) { + if len(cmdResult.ScaResults) <= 1 { + summary.Scans = GetScanSummaryByTargets(cmdResult, includeVulnerabilities, includeViolations) + return + } + for _, scaScan := range cmdResult.ScaResults { + summary.Scans = append(summary.Scans, GetScanSummaryByTargets(cmdResult, includeVulnerabilities, includeViolations, scaScan.Target)...) + } + return +} + +func GetScanSummaryByTargets(r *Results, includeVulnerabilities, includeViolations bool, targets ...string) (summaries []formats.ScanSummary) { + if len(targets) == 0 { + // No filter, one scan summary for all targets + summaries = append(summaries, getScanSummary(includeVulnerabilities, includeViolations, r.ExtendedScanResults, r.ScaResults...)) + return + } + for _, target := range targets { + // Get target sca results + targetScaResults := []*ScaScanResult{} + if targetScaResult := r.getScaScanResultByTarget(target); targetScaResult != nil { + targetScaResults = append(targetScaResults, targetScaResult) + } + // Get target extended results + targetExtendedResults := r.ExtendedScanResults + if targetExtendedResults != nil { + targetExtendedResults = targetExtendedResults.GetResultsForTarget(target) + } + summaries = append(summaries, getScanSummary(includeVulnerabilities, includeViolations, targetExtendedResults, targetScaResults...)) + } + return +} + +func getScanSummary(includeVulnerabilities, includeViolations bool, extendedScanResults *ExtendedScanResults, scaResults ...*ScaScanResult) (summary formats.ScanSummary) { + if len(scaResults) == 1 { + summary.Target = scaResults[0].Target + } + if includeViolations { + summary.Violations = getScanViolationsSummary(extendedScanResults, scaResults...) + } + if includeVulnerabilities { + summary.Vulnerabilities = getScanSecurityVulnerabilitiesSummary(extendedScanResults, scaResults...) + } + return +} + +func getScanViolationsSummary(extendedScanResults *ExtendedScanResults, scaResults ...*ScaScanResult) (violations *formats.ScanViolationsSummary) { + watches := datastructures.MakeSet[string]() + parsed := datastructures.MakeSet[string]() + failBuild := false + scanIds := []string{} + moreInfoUrls := []string{} + violationsUniqueFindings := map[ViolationIssueType]formats.ResultSummary{} + // Parse unique findings + for _, scaResult := range scaResults { + for _, xrayResult := range scaResult.XrayResults { + if xrayResult.ScanId != "" { + scanIds = append(scanIds, xrayResult.ScanId) + } + if xrayResult.XrayDataUrl != "" { + moreInfoUrls = append(moreInfoUrls, xrayResult.XrayDataUrl) + } + for _, violation := range xrayResult.Violations { + watches.Add(violation.WatchName) + failBuild = failBuild || violation.FailBuild + key := violation.IssueId + violation.WatchName + if parsed.Exists(key) { + continue + } + parsed.Add(key) + severity := severityutils.GetSeverity(violation.Severity).String() + violationType := ViolationIssueType(violation.ViolationType) + if _, ok := violationsUniqueFindings[violationType]; !ok { + violationsUniqueFindings[violationType] = formats.ResultSummary{} + } + if _, ok := violationsUniqueFindings[violationType][severity]; !ok { + violationsUniqueFindings[violationType][severity] = map[string]int{} + } + if violationType == ViolationTypeSecurity { + applicableRuns := []*sarif.Run{} + if extendedScanResults != nil { + applicableRuns = append(applicableRuns, extendedScanResults.ApplicabilityScanResults...) + } + violationsUniqueFindings[violationType][severity] = mergeMaps(violationsUniqueFindings[violationType][severity], getSecuritySummaryFindings(violation.Cves, violation.IssueId, violation.Components, applicableRuns...)) + } else { + // License, Operational Risk + violationsUniqueFindings[violationType][severity][formats.NoStatus] += 1 + } + } + } + } + violations = &formats.ScanViolationsSummary{ + Watches: watches.ToSlice(), + FailBuild: failBuild, + ScanResultSummary: formats.ScanResultSummary{ScaResults: &formats.ScaScanResultSummary{ + ScanIds: scanIds, + MoreInfoUrls: moreInfoUrls, + Security: violationsUniqueFindings[ViolationTypeSecurity], + License: violationsUniqueFindings[ViolationTypeLicense], + OperationalRisk: violationsUniqueFindings[ViolationTypeOperationalRisk], + }, + }} + return +} + +func getScanSecurityVulnerabilitiesSummary(extendedScanResults *ExtendedScanResults, scaResults ...*ScaScanResult) (vulnerabilities *formats.ScanResultSummary) { + vulnerabilities = &formats.ScanResultSummary{} + parsed := datastructures.MakeSet[string]() + for _, scaResult := range scaResults { + for _, xrayResult := range scaResult.XrayResults { + if vulnerabilities.ScaResults == nil { + vulnerabilities.ScaResults = &formats.ScaScanResultSummary{Security: formats.ResultSummary{}} + } + if xrayResult.ScanId != "" { + vulnerabilities.ScaResults.ScanIds = append(vulnerabilities.ScaResults.ScanIds, xrayResult.ScanId) + } + if xrayResult.XrayDataUrl != "" { + vulnerabilities.ScaResults.MoreInfoUrls = append(vulnerabilities.ScaResults.MoreInfoUrls, xrayResult.XrayDataUrl) + } + for _, vulnerability := range xrayResult.Vulnerabilities { + if parsed.Exists(vulnerability.IssueId) { + continue + } + parsed.Add(vulnerability.IssueId) + severity := severityutils.GetSeverity(vulnerability.Severity).String() + applicableRuns := []*sarif.Run{} + if extendedScanResults != nil { + applicableRuns = append(applicableRuns, extendedScanResults.ApplicabilityScanResults...) + } + vulnerabilities.ScaResults.Security[severity] = mergeMaps(vulnerabilities.ScaResults.Security[severity], getSecuritySummaryFindings(vulnerability.Cves, vulnerability.IssueId, vulnerability.Components, applicableRuns...)) + } + } + } + if extendedScanResults == nil { + return + } + vulnerabilities.IacResults = getJasSummaryFindings(extendedScanResults.IacScanResults...) + vulnerabilities.SecretsResults = getJasSummaryFindings(extendedScanResults.SecretsScanResults...) + vulnerabilities.SastResults = getJasSummaryFindings(extendedScanResults.SastScanResults...) + return +} + +func getSecuritySummaryFindings(cves []services.Cve, issueId string, components map[string]services.Component, applicableRuns ...*sarif.Run) map[string]int { + uniqueFindings := map[string]int{} + for _, cve := range cves { + applicableStatus := jasutils.NotScanned + if applicableInfo := getCveApplicabilityField(getCveId(cve, issueId), applicableRuns, components); applicableInfo != nil { + applicableStatus = jasutils.ConvertToApplicabilityStatus(applicableInfo.Status) + } + uniqueFindings[applicableStatus.String()] += 1 + } + if len(cves) == 0 { + // XRAY-ID, no scanners for them + uniqueFindings[jasutils.NotCovered.String()] += 1 + } + return uniqueFindings +} + +func getCveId(cve services.Cve, defaultIssueId string) string { + if cve.Id == "" { + return defaultIssueId + } + return cve.Id +} + +func mergeMaps(m1, m2 map[string]int) map[string]int { + if m1 == nil { + return m2 + } + for k, v := range m2 { + m1[k] += v + } + return m1 +} + +func getJasSummaryFindings(runs ...*sarif.Run) *formats.ResultSummary { + if len(runs) == 0 { + return nil + } + summary := formats.ResultSummary{} + for _, run := range runs { + for _, result := range run.Results { + resultLevel := sarifutils.GetResultLevel(result) + severity, err := severityutils.ParseSeverity(resultLevel, true) + if err != nil { + log.Warn(fmt.Sprintf("Failed to parse Sarif level %s. %s", resultLevel, err.Error())) + severity = severityutils.Unknown + } + if _, ok := summary[severity.String()]; !ok { + summary[severity.String()] = map[string]int{} + } + summary[severity.String()][formats.NoStatus] += len(result.Locations) + } + } + return &summary +} diff --git a/utils/resultwriter_test.go b/utils/resultwriter_test.go index 05b69bc4..0c9a5a62 100644 --- a/utils/resultwriter_test.go +++ b/utils/resultwriter_test.go @@ -3,6 +3,7 @@ package utils import ( "os" "path/filepath" + "sort" "testing" "github.com/jfrog/jfrog-cli-core/v2/utils/tests" @@ -441,3 +442,154 @@ func TestJSONMarshall(t *testing.T) { }) } } + +func TestGetSummary(t *testing.T) { + dummyExtendedScanResults := &ExtendedScanResults{ + ApplicabilityScanResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResults(sarifutils.CreateDummyPassingResult("applic_CVE-2")).WithInvocations([]*sarif.Invocation{ + sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation("target1")), + }), + }, + SecretsScanResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResults(sarifutils.CreateResultWithLocations("", "", "note", sarifutils.CreateLocation("target1/file", 0, 0, 0, 0, "snippet"))).WithInvocations([]*sarif.Invocation{ + sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation("target1")), + }), + sarifutils.CreateRunWithDummyResults(sarifutils.CreateResultWithLocations("", "", "note", sarifutils.CreateLocation("target2/file", 0, 0, 0, 0, "snippet"))).WithInvocations([]*sarif.Invocation{ + sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation("target2")), + }), + }, + SastScanResults: []*sarif.Run{ + sarifutils.CreateRunWithDummyResults(sarifutils.CreateResultWithLocations("", "", "note", sarifutils.CreateLocation("target1/file2", 0, 0, 0, 0, "snippet"))).WithInvocations([]*sarif.Invocation{ + sarif.NewInvocation().WithWorkingDirectory(sarif.NewSimpleArtifactLocation("target1")), + }), + }, + } + + expectedVulnerabilities := &formats.ScanResultSummary{ + ScaResults: &formats.ScaScanResultSummary{ + ScanIds: []string{TestScaScanId}, + MoreInfoUrls: []string{TestMoreInfoUrl}, + Security: formats.ResultSummary{ + "Critical": map[string]int{jasutils.ApplicabilityUndetermined.String(): 1}, + "High": map[string]int{jasutils.NotApplicable.String(): 1}, + }, + }, + SecretsResults: &formats.ResultSummary{"Low": map[string]int{jasutils.NotScanned.String(): 2}}, + SastResults: &formats.ResultSummary{"Low": map[string]int{jasutils.NotScanned.String(): 1}}, + } + expectedViolations := &formats.ScanViolationsSummary{ + Watches: []string{"test-watch-name", "test-watch-name2"}, + FailBuild: true, + ScanResultSummary: formats.ScanResultSummary{ + ScaResults: &formats.ScaScanResultSummary{ + ScanIds: []string{TestScaScanId}, + MoreInfoUrls: []string{TestMoreInfoUrl}, + Security: formats.ResultSummary{ + "Critical": map[string]int{jasutils.ApplicabilityUndetermined.String(): 1}, + "High": map[string]int{jasutils.NotApplicable.String(): 1}, + }, + License: formats.ResultSummary{"High": map[string]int{jasutils.NotScanned.String(): 1}}, + }, + }, + } + + testCases := []struct { + name string + results *Results + includeVulnerabilities bool + includeViolations bool + expected formats.ResultsSummary + }{ + { + name: "Vulnerabilities only", + includeVulnerabilities: true, + results: &Results{ + ScaResults: []*ScaScanResult{{ + Target: "target1", + XrayResults: getDummyScaTestResults(true, true), + }}, + ExtendedScanResults: dummyExtendedScanResults, + }, + expected: formats.ResultsSummary{ + Scans: []formats.ScanSummary{{ + Target: "target1", + Vulnerabilities: expectedVulnerabilities, + }}, + }, + }, + { + name: "Violations only", + includeViolations: true, + results: &Results{ + ScaResults: []*ScaScanResult{{ + Target: "target1", + XrayResults: getDummyScaTestResults(true, true), + }}, + ExtendedScanResults: dummyExtendedScanResults, + }, + expected: formats.ResultsSummary{ + Scans: []formats.ScanSummary{{ + Target: "target1", + Violations: expectedViolations, + }}, + }, + }, + { + name: "Vulnerabilities and Violations", + includeVulnerabilities: true, + includeViolations: true, + results: &Results{ + ScaResults: []*ScaScanResult{{ + Target: "target1", + XrayResults: getDummyScaTestResults(true, true), + }}, + ExtendedScanResults: dummyExtendedScanResults, + }, + expected: formats.ResultsSummary{ + Scans: []formats.ScanSummary{{ + Target: "target1", + Violations: expectedViolations, + Vulnerabilities: expectedVulnerabilities, + }}, + }, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + summary := ToSummary(testCase.results, testCase.includeVulnerabilities, testCase.includeViolations) + for _, scan := range summary.Scans { + if scan.Vulnerabilities != nil { + sort.Strings(scan.Vulnerabilities.ScaResults.ScanIds) + sort.Strings(scan.Vulnerabilities.ScaResults.MoreInfoUrls) + } + if scan.Violations != nil { + sort.Strings(scan.Violations.Watches) + sort.Strings(scan.Violations.ScanResultSummary.ScaResults.ScanIds) + sort.Strings(scan.Violations.ScanResultSummary.ScaResults.MoreInfoUrls) + } + } + assert.Equal(t, testCase.expected, summary) + }) + } +} + +func getDummyScaTestResults(vulnerability, violation bool) (responses []services.ScanResponse) { + response := services.ScanResponse{} + if vulnerability { + response.Vulnerabilities = []services.Vulnerability{ + {IssueId: "XRAY-1", Severity: "Critical", Cves: []services.Cve{{Id: "CVE-1"}}, Components: map[string]services.Component{"issueId_direct_dependency": {}}}, + {IssueId: "XRAY-2", Severity: "High", Cves: []services.Cve{{Id: "CVE-2"}}, Components: map[string]services.Component{"issueId_direct_dependency": {}}}, + } + } + if violation { + response.Violations = []services.Violation{ + {ViolationType: ViolationTypeSecurity.String(), WatchName: "test-watch-name", IssueId: "XRAY-1", Severity: "Critical", Cves: []services.Cve{{Id: "CVE-1"}}, Components: map[string]services.Component{"issueId_direct_dependency": {}}}, + {ViolationType: ViolationTypeSecurity.String(), FailBuild: true, WatchName: "test-watch-name", IssueId: "XRAY-2", Severity: "High", Cves: []services.Cve{{Id: "CVE-2"}}, Components: map[string]services.Component{"issueId_direct_dependency": {}}}, + {ViolationType: ViolationTypeLicense.String(), WatchName: "test-watch-name2", IssueId: "MIT", Severity: "High", LicenseKey: "MIT", Components: map[string]services.Component{"issueId_direct_dependency": {}}}, + } + } + response.ScanId = TestScaScanId + response.XrayDataUrl = TestMoreInfoUrl + responses = append(responses, response) + return +} diff --git a/utils/securityJobSummary.go b/utils/securityJobSummary.go index f94724c9..a1a4be41 100644 --- a/utils/securityJobSummary.go +++ b/utils/securityJobSummary.go @@ -1,306 +1,305 @@ package utils import ( + "errors" "fmt" "path/filepath" "sort" "strings" - "github.com/jfrog/gofrog/datastructures" - "github.com/jfrog/jfrog-cli-core/v2/commandsummary" + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" + + "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils/commandsummary" + "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-security/formats" - "github.com/jfrog/jfrog-cli-security/formats/sarifutils" + "github.com/jfrog/jfrog-cli-security/resources" "github.com/jfrog/jfrog-cli-security/utils/jasutils" "github.com/jfrog/jfrog-cli-security/utils/severityutils" - "github.com/jfrog/jfrog-client-go/utils/log" - "github.com/jfrog/jfrog-client-go/xray/services" - "github.com/owenrumney/go-sarif/v2/sarif" - "golang.org/x/exp/maps" ) const ( - Build SecuritySummarySection = "Builds" - Binary SecuritySummarySection = "Artifacts" - Modules SecuritySummarySection = "Modules" - Curation SecuritySummarySection = "Curation" + Build SecuritySummarySection = "Build-info Scans" + Binary SecuritySummarySection = "Artifact Scans" + Modules SecuritySummarySection = "Source Code Scans" + Docker SecuritySummarySection = "Docker Image Scans" + Curation SecuritySummarySection = "Curation Audit" + + PreFormat HtmlTag = "
šø Unlock detailed findings
%s" + ImgTag HtmlTag = "" + CenterContent HtmlTag = "
%s", issueDetails) + if !strings.HasPrefix(scan.Target, wd) { + continue + } + if scan.Target == wd { + summary.Scans[i].Target = filepath.Base(wd) + } + summary.Scans[i].Target = strings.TrimPrefix(scan.Target, wd) } - return fmt.Sprintf("| ā | %s |
%s|", summary.Target, issueDetails) } -func getDetailsString(summary formats.ScanSummaryResult) string { - // If summary includes curation issues, then it means only curation issues are in this summary, no need to continue - if summary.HasBlockedCuration() { - return getBlockedCurationSummaryString(summary) - } - violationContent := getViolationSummaryString(summary) - vulnerabilitiesContent := getVulnerabilitiesSummaryString(summary) - delimiter := "" - if len(violationContent) > 0 && len(vulnerabilitiesContent) > 0 { - delimiter = "