From e794ab8b45221463e6bd9a27ff8267532df917df Mon Sep 17 00:00:00 2001 From: Daniel Grunberger Date: Sun, 30 Jul 2023 22:20:04 +0300 Subject: [PATCH] changes --- cmd/scan/framework.go | 2 +- cmd/scan/image.go | 4 - core/core/scan.go | 37 +- core/pkg/resourcehandler/filesloader.go | 67 +-- .../resourcehandler/handlerpullresources.go | 2 +- core/pkg/resourcehandler/interface.go | 3 +- core/pkg/resourcehandler/k8sresources.go | 3 +- .../resultshandling/printer/v2/jsonprinter.go | 30 +- .../printer/v2/prettyprinter.go | 40 +- .../printer/v2/prettyprinter/clusterscan.go | 3 +- .../printer/v2/prettyprinter/imagescan.go | 2 +- .../printer/v2/prettyprinter/reposcan.go | 7 +- .../configurationprinter/categorytable.go | 20 +- .../categorytable_test.go | 4 +- .../configurationprinter/clusterscan.go | 6 +- .../configurationprinter/clusterscan_test.go | 90 ++++ .../configurationprinter/datastructures.go | 5 + .../configurationprinter/reposcan.go | 6 +- .../configurationprinter/reposcan_test.go | 102 ++++ .../configurationprinter/utils.go | 16 + .../configurationprinter/utils_test.go | 8 +- .../configurationprinter/workloadscan.go | 23 +- .../configurationprinter/workloadscan_test.go | 116 +++++ .../imageprinter/datastructures.go | 1 + .../tableprinter/imageprinter/tablewriter.go | 5 +- .../tableprinter/imageprinter/utils_test.go | 6 +- .../tableprinter/utils/utils_test.go | 11 + .../printer/v2/prettyprinter/utils.go | 61 ++- .../printer/v2/prettyprinter/workloadscan.go | 15 +- .../printer/v2/prettyprinter_test.go | 1 + core/pkg/resultshandling/printer/v2/utils.go | 66 +-- .../resultshandling/printer/v2/utils_test.go | 480 ++++++++++++++++++ 32 files changed, 1007 insertions(+), 235 deletions(-) create mode 100644 core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/clusterscan_test.go create mode 100644 core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/reposcan_test.go create mode 100644 core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/workloadscan_test.go create mode 100644 core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/utils/utils_test.go create mode 100644 core/pkg/resultshandling/printer/v2/prettyprinter_test.go create mode 100644 core/pkg/resultshandling/printer/v2/utils_test.go diff --git a/cmd/scan/framework.go b/cmd/scan/framework.go index 81323ba1..9628161b 100644 --- a/cmd/scan/framework.go +++ b/cmd/scan/framework.go @@ -119,7 +119,7 @@ func getFrameworkCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Comm if err = results.HandleResults(ctx); err != nil { logger.L().Fatal(err.Error()) } - if !scanInfo.VerboseMode { + if !scanInfo.VerboseMode && scanInfo.ScanType == "" { logger.L().Info("Run with '--verbose'/'-v' flag for detailed resources view\n") } if results.GetRiskScore() > float32(scanInfo.FailThreshold) { diff --git a/cmd/scan/image.go b/cmd/scan/image.go index c5d5ad2d..3cbea5cb 100644 --- a/cmd/scan/image.go +++ b/cmd/scan/image.go @@ -75,10 +75,6 @@ func getImageCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Command resultsHandler.HandleResults(ctx) - if !scanInfo.VerboseMode { - logger.L().Info("Run with '--verbose'/'-v' for the full report\n") - } - if imagescan.ExceedsSeverityThreshold(scanResults, failOnSeverity) { terminateOnExceedingSeverity(scanInfo, logger.L()) } diff --git a/core/core/scan.go b/core/core/scan.go index 8bd6e905..63cf0461 100644 --- a/core/core/scan.go +++ b/core/core/scan.go @@ -221,31 +221,38 @@ func (ks *Kubescape) Scan(ctx context.Context, scanInfo *cautils.ScanInfo) (*res } func scanImages(scanInfo *cautils.ScanInfo, scanData *cautils.OPASessionObj, ctx context.Context, resultsHandling *resultshandling.ResultsHandler) { + imagesToScan := []string{} + + if scanInfo.ScanType == cautils.ScanTypeWorkload { + containers, _ := workloadinterface.NewWorkloadObj(scanData.ScannedWorkload.GetObject()).GetContainers() + for _, container := range containers { + imagesToScan = append(imagesToScan, container.Image) + } + } else { + for _, workload := range scanData.AllResources { + containers, _ := workloadinterface.NewWorkloadObj(workload.GetObject()).GetContainers() + for _, container := range containers { + imagesToScan = append(imagesToScan, container.Image) + } + } + } + progressListener := cautils.NewProgressHandler("") + progressListener.Start(len(imagesToScan)) + defer progressListener.Stop() + logger.L().Info("Scanning images") dbCfg, _ := imagescan.NewDefaultDBConfig() svc := imagescan.NewScanService(dbCfg) - if scanInfo.ScanType == cautils.ScanTypeWorkload { - wlObj := workloadinterface.NewWorkloadObj(scanData.ScannedWorkload.GetObject()) - scanSingleWorkload(wlObj, ctx, svc, resultsHandling, scanInfo) - } else { - for _, workload := range scanData.AllResources { - wlObj := workloadinterface.NewWorkloadObj(workload.GetObject()) - scanSingleWorkload(wlObj, ctx, svc, resultsHandling, scanInfo) - } + for _, img := range imagesToScan { + scanSingleImage(ctx, img, svc, resultsHandling, *scanInfo) + progressListener.ProgressJob(1, fmt.Sprintf("image name: %s", img)) } logger.L().Success("Finished scanning images") } -func scanSingleWorkload(wlObj *workloadinterface.Workload, ctx context.Context, svc imagescan.Service, resultsHandling *resultshandling.ResultsHandler, scanInfo *cautils.ScanInfo) { - containers, _ := wlObj.GetContainers() - for _, container := range containers { - scanSingleImage(ctx, container.Image, svc, resultsHandling, *scanInfo) - } -} - func scanSingleImage(ctx context.Context, img string, svc imagescan.Service, resultsHandling *resultshandling.ResultsHandler, scanInfo cautils.ScanInfo) { logger.L().Ctx(ctx).Debug(fmt.Sprintf("Scanning image: %s", img)) diff --git a/core/pkg/resourcehandler/filesloader.go b/core/pkg/resourcehandler/filesloader.go index b1e49680..5a999993 100644 --- a/core/pkg/resourcehandler/filesloader.go +++ b/core/pkg/resourcehandler/filesloader.go @@ -5,8 +5,8 @@ import ( "fmt" "os" "path/filepath" + "strings" - "github.com/armosec/armoapi-go/identifiers" "github.com/kubescape/k8s-interface/workloadinterface" "github.com/kubescape/opa-utils/reporthandling" "k8s.io/apimachinery/pkg/version" @@ -32,7 +32,7 @@ func NewFileResourceHandler(_ context.Context, inputPatterns []string, workloadI } } -func (fileHandler *FileResourceHandler) GetResources(ctx context.Context, sessionObj *cautils.OPASessionObj, _ *identifiers.PortalDesignator, progressListener opaprocessor.IJobProgressNotificationClient, scanInfo cautils.ScanInfo) (cautils.K8SResources, map[string]workloadinterface.IMetadata, cautils.KSResources, map[string]bool, error) { +func (fileHandler *FileResourceHandler) GetResources(ctx context.Context, sessionObj *cautils.OPASessionObj, progressListener opaprocessor.IJobProgressNotificationClient, scanInfo cautils.ScanInfo) (cautils.K8SResources, map[string]workloadinterface.IMetadata, cautils.KSResources, map[string]bool, error) { allResources := map[string]workloadinterface.IMetadata{} ksResources := cautils.KSResources{} @@ -112,13 +112,13 @@ func getWorkloadFromHelmChart(ctx context.Context, helmPath, workloadPath string defer os.RemoveAll(clonedRepo) } + // Get repo root + repoRoot, gitRepo := extractGitRepo(helmPath) + helmSourceToWorkloads, helmSourceToChartName := cautils.LoadResourcesFromHelmCharts(ctx, helmPath) wlSource, _ := helmSourceToWorkloads[workloadPath] - // Get repo root - repoRoot, gitRepo := extractGitRepo(helmPath) - helmChartName := helmSourceToChartName[workloadPath] relSource, err := filepath.Rel(repoRoot, helmPath) @@ -141,64 +141,14 @@ func getWorkloadFromHelmChart(ctx context.Context, helmPath, workloadPath string } workloadSource := reporthandling.Source{ + Path: repoRoot, + HelmPath: strings.TrimPrefix(workloadPath, fmt.Sprintf("%s/", repoRoot)), RelativePath: helmPath, FileType: reporthandling.SourceTypeHelmChart, HelmChartName: helmChartName, LastCommit: lastCommit, } - workloadIDToSource := make(map[string]reporthandling.Source, 0) - workloadIDToSource[wlSource[0].GetID()] = workloadSource - workloads := []workloadinterface.IMetadata{} - workloads = append(workloads, wlSource...) - - return workloadIDToSource, workloads, nil - -} - -func getWorkloadFromHelmChart(ctx context.Context, helmPath, workloadPath string) (map[string]reporthandling.Source, []workloadinterface.IMetadata, error) { - clonedRepo, err := cloneGitRepo(&helmPath) - if err != nil { - return nil, nil, err - } - if clonedRepo != "" { - defer os.RemoveAll(clonedRepo) - } - - helmSourceToWorkloads, helmSourceToChartName := cautils.LoadResourcesFromHelmCharts(ctx, helmPath) - - wlSource, _ := helmSourceToWorkloads[workloadPath] - - // Get repo root - repoRoot, gitRepo := extractGitRepo(helmPath) - - helmChartName := helmSourceToChartName[workloadPath] - - relSource, err := filepath.Rel(repoRoot, helmPath) - if err == nil { - helmPath = relSource - } - - var lastCommit reporthandling.LastCommit - if gitRepo != nil { - commitInfo, _ := gitRepo.GetFileLastCommit(helmPath) - if commitInfo != nil { - lastCommit = reporthandling.LastCommit{ - Hash: commitInfo.SHA, - Date: commitInfo.Author.Date, - CommitterName: commitInfo.Author.Name, - CommitterEmail: commitInfo.Author.Email, - Message: commitInfo.Message, - } - } - } - - workloadSource := reporthandling.Source{ - RelativePath: helmPath, - FileType: reporthandling.SourceTypeHelmChart, - HelmChartName: helmChartName, - LastCommit: lastCommit, - } workloadIDToSource := make(map[string]reporthandling.Source, 0) workloadIDToSource[wlSource[0].GetID()] = workloadSource @@ -275,6 +225,7 @@ func getResourcesFromPath(ctx context.Context, path string) (map[string]reportha } workloadSource := reporthandling.Source{ + Path: repoRoot, RelativePath: relSource, FileType: filetype, LastCommit: lastCommit, @@ -315,6 +266,8 @@ func getResourcesFromPath(ctx context.Context, path string) (map[string]reportha } workloadSource := reporthandling.Source{ + Path: repoRoot, + HelmPath: strings.TrimPrefix(path, fmt.Sprintf("%s/", repoRoot)), RelativePath: source, FileType: reporthandling.SourceTypeHelmChart, HelmChartName: helmChartName, diff --git a/core/pkg/resourcehandler/handlerpullresources.go b/core/pkg/resourcehandler/handlerpullresources.go index ce1be5ea..68f946f1 100644 --- a/core/pkg/resourcehandler/handlerpullresources.go +++ b/core/pkg/resourcehandler/handlerpullresources.go @@ -29,7 +29,7 @@ func CollectResources(ctx context.Context, rsrcHandler IResourceHandler, policyI setCloudMetadata(opaSessionObj) } - resourcesMap, allResources, ksResources, excludedRulesMap, err := rsrcHandler.GetResources(ctx, opaSessionObj, &policyIdentifier[0].Designators, progressListener, scanInfo) + resourcesMap, allResources, ksResources, excludedRulesMap, err := rsrcHandler.GetResources(ctx, opaSessionObj, progressListener, scanInfo) if err != nil { return err } diff --git a/core/pkg/resourcehandler/interface.go b/core/pkg/resourcehandler/interface.go index aaab17db..5a353cf5 100644 --- a/core/pkg/resourcehandler/interface.go +++ b/core/pkg/resourcehandler/interface.go @@ -3,7 +3,6 @@ package resourcehandler import ( "context" - "github.com/armosec/armoapi-go/identifiers" "github.com/kubescape/k8s-interface/workloadinterface" "github.com/kubescape/kubescape/v2/core/cautils" "github.com/kubescape/kubescape/v2/core/pkg/opaprocessor" @@ -11,7 +10,7 @@ import ( ) type IResourceHandler interface { - GetResources(context.Context, *cautils.OPASessionObj, *identifiers.PortalDesignator, opaprocessor.IJobProgressNotificationClient, cautils.ScanInfo) (cautils.K8SResources, map[string]workloadinterface.IMetadata, cautils.KSResources, map[string]bool, error) + GetResources(context.Context, *cautils.OPASessionObj, opaprocessor.IJobProgressNotificationClient, cautils.ScanInfo) (cautils.K8SResources, map[string]workloadinterface.IMetadata, cautils.KSResources, map[string]bool, error) GetClusterAPIServerInfo(ctx context.Context) *version.Info GetWorkloadParentKind(workloadinterface.IWorkload) string } diff --git a/core/pkg/resourcehandler/k8sresources.go b/core/pkg/resourcehandler/k8sresources.go index 0f315a87..8843c1c0 100644 --- a/core/pkg/resourcehandler/k8sresources.go +++ b/core/pkg/resourcehandler/k8sresources.go @@ -5,7 +5,6 @@ import ( "fmt" "strings" - "github.com/armosec/armoapi-go/identifiers" logger "github.com/kubescape/go-logger" "github.com/kubescape/go-logger/helpers" "github.com/kubescape/kubescape/v2/core/cautils" @@ -58,7 +57,7 @@ func NewK8sResourceHandler(k8s *k8sinterface.KubernetesApi, fieldSelector IField } } -func (k8sHandler *K8sResourceHandler) GetResources(ctx context.Context, sessionObj *cautils.OPASessionObj, designator *identifiers.PortalDesignator, progressListener opaprocessor.IJobProgressNotificationClient, scanInfo cautils.ScanInfo) (cautils.K8SResources, map[string]workloadinterface.IMetadata, cautils.KSResources, map[string]bool, error) { +func (k8sHandler *K8sResourceHandler) GetResources(ctx context.Context, sessionObj *cautils.OPASessionObj, progressListener opaprocessor.IJobProgressNotificationClient, scanInfo cautils.ScanInfo) (cautils.K8SResources, map[string]workloadinterface.IMetadata, cautils.KSResources, map[string]bool, error) { // get k8s resources logger.L().Info("Accessing Kubernetes objects") diff --git a/core/pkg/resultshandling/printer/v2/jsonprinter.go b/core/pkg/resultshandling/printer/v2/jsonprinter.go index ea21cf9c..a319c88b 100644 --- a/core/pkg/resultshandling/printer/v2/jsonprinter.go +++ b/core/pkg/resultshandling/printer/v2/jsonprinter.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "io" "os" "path/filepath" "strings" @@ -15,7 +14,6 @@ import ( "github.com/kubescape/go-logger/helpers" "github.com/kubescape/kubescape/v2/core/cautils" "github.com/kubescape/kubescape/v2/core/pkg/resultshandling/printer" - reporthandlingv2 "github.com/kubescape/opa-utils/reporthandling/v2" ) const ( @@ -48,35 +46,9 @@ func (jp *JsonPrinter) Score(score float32) { } -func printImageAndConfigurationScanning(output io.Writer, imageScanData *models.PresenterConfig, opaSessionObj *cautils.OPASessionObj) error { - type Document struct { - *models.Document `json:",omitempty"` - *reporthandlingv2.PostureReport `json:",omitempty"` - } - - doc, err := models.NewDocument(imageScanData.Packages, imageScanData.Context, imageScanData.Matches, imageScanData.IgnoredMatches, imageScanData.MetadataProvider, - imageScanData.AppConfig, imageScanData.DBStatus) - if err != nil { - return err - } - - docForJson := Document{ - &doc, - FinalizeResults(opaSessionObj), - } - - enc := json.NewEncoder(output) - // prevent > and < from being escaped in the payload - enc.SetEscapeHTML(false) - enc.SetIndent("", " ") - return enc.Encode(&docForJson) -} - func (jp *JsonPrinter) ActionPrint(ctx context.Context, opaSessionObj *cautils.OPASessionObj, imageScanData []cautils.ImageScanData) { var err error - if opaSessionObj != nil && imageScanData != nil { - err = printImageAndConfigurationScanning(jp.writer, imageScanData[0].PresenterConfig, opaSessionObj) - } else if opaSessionObj != nil { + if opaSessionObj != nil { err = printConfigurationsScanning(opaSessionObj, ctx, jp) } else if imageScanData != nil { err = jp.PrintImageScan(ctx, imageScanData[0].PresenterConfig) diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter.go b/core/pkg/resultshandling/printer/v2/prettyprinter.go index 4673cb32..960806aa 100644 --- a/core/pkg/resultshandling/printer/v2/prettyprinter.go +++ b/core/pkg/resultshandling/printer/v2/prettyprinter.go @@ -53,10 +53,6 @@ func NewPrettyPrinter(verboseMode bool, formatVersion string, attackTree bool, v return prettyPrinter } -func (pp *PrettyPrinter) PrintNextSteps() { - pp.mainPrinter.PrintNextSteps() -} - func (pp *PrettyPrinter) SetMainPrinter() { switch pp.scanType { case cautils.ScanTypeCluster: @@ -72,6 +68,10 @@ func (pp *PrettyPrinter) SetMainPrinter() { } } +func (pp *PrettyPrinter) PrintNextSteps() { + pp.mainPrinter.PrintNextSteps() +} + // convertToImageScanSummary takes a list of image scan data and converts it to a single image scan summary func (pp *PrettyPrinter) convertToImageScanSummary(imageScanData []cautils.ImageScanData) (*imageprinter.ImageScanSummary, error) { imageScanSummary := imageprinter.ImageScanSummary{ @@ -90,14 +90,11 @@ func (pp *PrettyPrinter) convertToImageScanSummary(imageScanData []cautils.Image continue } - cves := extractCVEs(doc) - imageScanSummary.CVEs = append(imageScanSummary.CVEs, cves...) + imageScanSummary.CVEs = extractCVEs(doc.Matches) - mapPackageNameToScore := extractPkgNameToScore(doc) - insertPackageScoresIntoMap(mapPackageNameToScore, imageScanSummary) + imageScanSummary.PackageScores = extractPkgNameToScoreMap(doc.Matches) - mapSeverityToSummary := extractSeverityToSummaryMap(cves) - insertSeveritiesSummariesIntoMap(mapSeverityToSummary, imageScanSummary) + imageScanSummary.MapsSeverityToSummary = extractSeverityToSummaryMap(imageScanSummary.CVEs) } return &imageScanSummary, nil @@ -127,9 +124,7 @@ func (pp *PrettyPrinter) ActionPrint(_ context.Context, opaSessionObj *cautils.O } } - if pp.scanType == cautils.ScanTypeWorkload { - cautils.InfoDisplay(pp.writer, "Workload: %s/%s/%s\n", opaSessionObj.ScannedWorkload.GetNamespace(), opaSessionObj.ScannedWorkload.GetKind(), opaSessionObj.ScannedWorkload.GetName()) - } + pp.printOverview(opaSessionObj, pp.verboseMode) pp.mainPrinter.PrintConfigurationsScanning(&opaSessionObj.Report.SummaryDetails, sortedControlIDs) @@ -147,6 +142,23 @@ func (pp *PrettyPrinter) ActionPrint(_ context.Context, opaSessionObj *cautils.O } } +func (pp *PrettyPrinter) printOverview(opaSessionObj *cautils.OPASessionObj, printExtraLine bool) { + if printExtraLine { + fmt.Fprintf(pp.writer, "\n") + } + + if pp.scanType == cautils.ScanTypeCluster || pp.scanType == cautils.ScanTypeRepo { + cautils.InfoDisplay(pp.writer, "\nSecurity Overview\n\n") + } else if pp.scanType == cautils.ScanTypeWorkload { + ns := opaSessionObj.ScannedWorkload.GetNamespace() + if ns == "" { + cautils.InfoDisplay(pp.writer, "Workload - Kind: %s, Name: %s\n\n", opaSessionObj.ScannedWorkload.GetKind(), opaSessionObj.ScannedWorkload.GetName()) + } else { + cautils.InfoDisplay(pp.writer, "Workload - Namespace: %s, Kind: %s, Name: %s\n\n", opaSessionObj.ScannedWorkload.GetNamespace(), opaSessionObj.ScannedWorkload.GetKind(), opaSessionObj.ScannedWorkload.GetName()) + } + } +} + func (pp *PrettyPrinter) SetWriter(ctx context.Context, outputFile string) { // PrettyPrinter should accept Stdout at least by its full name (path) // and follow the common behavior of outputting to a default filename @@ -195,6 +207,7 @@ func (prettyPrinter *PrettyPrinter) printSummary(controlName string, controlSumm cautils.DescriptionDisplay(prettyPrinter.writer, "\n") } + func (prettyPrinter *PrettyPrinter) printTitle(controlSummary reportsummary.IControlSummary) { cautils.InfoDisplay(prettyPrinter.writer, "[control: %s - %s] ", controlSummary.GetName(), cautils.GetControlLink(controlSummary.GetID())) statusDetails := "" @@ -214,6 +227,7 @@ func (prettyPrinter *PrettyPrinter) printTitle(controlSummary reportsummary.ICon cautils.WarningDisplay(prettyPrinter.writer, "Reason: %v\n", controlSummary.GetStatus().Info()) } } + func (pp *PrettyPrinter) printResources(controlSummary reportsummary.IControlSummary, allResources map[string]workloadinterface.IMetadata) { workloadsSummary := listResultSummary(controlSummary, allResources) diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter/clusterscan.go b/core/pkg/resultshandling/printer/v2/prettyprinter/clusterscan.go index b623046c..95757d1a 100644 --- a/core/pkg/resultshandling/printer/v2/prettyprinter/clusterscan.go +++ b/core/pkg/resultshandling/printer/v2/prettyprinter/clusterscan.go @@ -26,6 +26,7 @@ var _ MainPrinter = &ClusterPrinter{} func (cp *ClusterPrinter) PrintImageScanning(summary *imageprinter.ImageScanSummary) { printImageScanningSummary(cp.writer, *summary, false) + printImagesCommands(cp.writer, *summary) } func (cp *ClusterPrinter) PrintConfigurationsScanning(summaryDetails *reportsummary.SummaryDetails, sortedControlIDs [][]string) { @@ -46,7 +47,7 @@ func (cp *ClusterPrinter) PrintNextSteps() { func (cp *ClusterPrinter) getNextSteps() []string { return []string{ - complianceScanRunText, + configScanVerboseRunText, installHelmText, } } diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter/imagescan.go b/core/pkg/resultshandling/printer/v2/prettyprinter/imagescan.go index 1fc7395b..9d866f35 100644 --- a/core/pkg/resultshandling/printer/v2/prettyprinter/imagescan.go +++ b/core/pkg/resultshandling/printer/v2/prettyprinter/imagescan.go @@ -48,5 +48,5 @@ func (ip *ImagePrinter) PrintConfigurationsScanning(summaryDetails *reportsummar } func (ip *ImagePrinter) PrintNextSteps() { - printNextSteps(ip.writer, []string{CICDSetupText, installHelmText}) + printNextSteps(ip.writer, []string{imageScanVerboseRunText, CICDSetupText, installHelmText}) } diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter/reposcan.go b/core/pkg/resultshandling/printer/v2/prettyprinter/reposcan.go index eab7fa0a..dd3a887e 100644 --- a/core/pkg/resultshandling/printer/v2/prettyprinter/reposcan.go +++ b/core/pkg/resultshandling/printer/v2/prettyprinter/reposcan.go @@ -28,6 +28,7 @@ var _ MainPrinter = &RepoPrinter{} func (rp *RepoPrinter) PrintImageScanning(summary *imageprinter.ImageScanSummary) { printImageScanningSummary(rp.writer, *summary, false) + printImagesCommands(rp.writer, *summary) printTopVulnerabilities(rp.writer, *summary) } @@ -47,6 +48,7 @@ func (rp *RepoPrinter) PrintNextSteps() { func (rp *RepoPrinter) getNextSteps() []string { return []string{ + configScanVerboseRunText, clusterScanRunText, CICDSetupText, installHelmText, @@ -72,10 +74,11 @@ func (rp *RepoPrinter) getWorkloadScanCommand(ns, kind, name string, source repo if ns == "" { cmd = fmt.Sprintf("$ kubescape scan workload %s/%s", kind, name) } + if source.FileType == reporthandling.SourceTypeHelmChart { - return fmt.Sprintf("%s --chart-path=%s --file-path=%s", cmd, source.Path, source.RelativePath) + return fmt.Sprintf("%s --chart-path=%s --file-path=%s", cmd, fmt.Sprintf("%s/%s", source.Path, source.HelmPath), fmt.Sprintf("%s/%s", source.Path, source.RelativePath)) } else { - return fmt.Sprintf("%s --file-path=%s", cmd, source.RelativePath) + return fmt.Sprintf("%s --file-path=%s", cmd, fmt.Sprintf("%s/%s", source.Path, source.RelativePath)) } } diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/categorytable.go b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/categorytable.go index 4deee376..a1522cc4 100644 --- a/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/categorytable.go +++ b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/categorytable.go @@ -6,6 +6,7 @@ import ( "github.com/kubescape/kubescape/v2/core/cautils" "github.com/kubescape/kubescape/v2/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/utils" + "github.com/kubescape/opa-utils/reporthandling/apis" "github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary" "github.com/olekukonko/tablewriter" ) @@ -72,12 +73,7 @@ func generateCategoryStatusRow(controlSummary reportsummary.IControlSummary, inf rows[0] = controlSummary.GetName() } - // skipped is shown as action required - if status.IsSkipped() { - rows[1] = fmt.Sprintf("%s %s", "action required", GetInfoColumn(controlSummary, infoToPrintInfo)) - } else { - rows[1] = string(controlSummary.GetStatus().Status()) - } + rows[1] = getStatus(status, controlSummary, infoToPrintInfo) rows[2] = getDocsForControl(controlSummary) @@ -85,6 +81,14 @@ func generateCategoryStatusRow(controlSummary reportsummary.IControlSummary, inf } +func getStatus(status apis.IStatus, controlSummary reportsummary.IControlSummary, infoToPrintInfo []utils.InfoStars) string { + // skipped is shown as action required + if status.IsSkipped() { + return fmt.Sprintf("%s %s", "action required", GetInfoColumn(controlSummary, infoToPrintInfo)) + } + return string(controlSummary.GetStatus().Status()) +} + func getCategoryTableWriter(writer io.Writer, headers []string, columnAligments []int) *tablewriter.Table { table := tablewriter.NewWriter(writer) table.SetHeader(headers) @@ -95,7 +99,7 @@ func getCategoryTableWriter(writer io.Writer, headers []string, columnAligments } func renderSingleCategory(writer io.Writer, categoryName string, table *tablewriter.Table, rows [][]string, infoToPrintInfo []utils.InfoStars) { - cautils.InfoTextDisplay(writer, "\n"+categoryName+"\n") + cautils.InfoTextDisplay(writer, categoryName+"\n") table.ClearRows() table.AppendBulk(rows) @@ -103,7 +107,7 @@ func renderSingleCategory(writer io.Writer, categoryName string, table *tablewri table.Render() if len(infoToPrintInfo) > 0 { - utils.PrintInfo(writer, infoToPrintInfo) + printCategoryInfo(writer, infoToPrintInfo) } cautils.SimpleDisplay(writer, "\n") diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/categorytable_test.go b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/categorytable_test.go index abb2305d..15f19aa2 100644 --- a/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/categorytable_test.go +++ b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/categorytable_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_initCategoryTableData(t *testing.T) { +func TestInitCategoryTableData(t *testing.T) { tests := []struct { name string categoryType CategoryType @@ -46,7 +46,7 @@ func Test_initCategoryTableData(t *testing.T) { } } -func Test_generateCategoryStatusRow(t *testing.T) { +func TestGenerateCategoryStatusRow(t *testing.T) { tests := []struct { name string controlSummary reportsummary.IControlSummary diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/clusterscan.go b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/clusterscan.go index 90c3539d..c4b1ec34 100644 --- a/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/clusterscan.go +++ b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/clusterscan.go @@ -30,13 +30,13 @@ func (cp *ClusterPrinter) PrintCategoriesTables(writer io.Writer, summaryDetails continue } - infoToPrintInfo := utils.MapInfoToPrintInfoFromIface(categoryControl.controlSummaries) - - cp.renderSingleCategoryTable(categoryControl.CategoryName, mapCategoryToType[id], writer, categoryControl.controlSummaries, infoToPrintInfo) + cp.renderSingleCategoryTable(categoryControl.CategoryName, mapCategoryToType[id], writer, categoryControl.controlSummaries, utils.MapInfoToPrintInfoFromIface(categoryControl.controlSummaries)) } } func (cp *ClusterPrinter) renderSingleCategoryTable(categoryName string, categoryType CategoryType, writer io.Writer, controlSummaries []reportsummary.IControlSummary, infoToPrintInfo []utils.InfoStars) { + sortControlSummaries(controlSummaries) + headers, columnAligments := initCategoryTableData(categoryType) table := getCategoryTableWriter(writer, headers, columnAligments) diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/clusterscan_test.go b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/clusterscan_test.go new file mode 100644 index 00000000..fdf4e918 --- /dev/null +++ b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/clusterscan_test.go @@ -0,0 +1,90 @@ +package configurationprinter + +import ( + "testing" + + "github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary" +) + +func TestClusterScan_GenerateCountingCategoryRow(t *testing.T) { + tests := []struct { + name string + controlSummary reportsummary.IControlSummary + expectedRow []string + }{ + { + name: "failed resources", + controlSummary: &reportsummary.ControlSummary{ + ControlID: "ctrl1", + Name: "ctrl1", + StatusCounters: reportsummary.StatusCounters{ + FailedResources: 5, + PassedResources: 3, + SkippedResources: 2, + }, + }, + expectedRow: []string{"ctrl1", "5", "$ kubescape scan control ctrl1 -v"}, + }, + { + name: "passed resources", + controlSummary: &reportsummary.ControlSummary{ + ControlID: "ctrl2", + Name: "ctrl2", + StatusCounters: reportsummary.StatusCounters{ + PassedResources: 3, + }, + }, + expectedRow: []string{"ctrl2", "0", "$ kubescape scan control ctrl2 -v"}, + }, + } + + clusterPrinter := NewClusterPrinter() + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + row := clusterPrinter.generateCountingCategoryRow(test.controlSummary) + + if len(row) != len(test.expectedRow) { + t.Errorf("expected row length %d, got %d", len(test.expectedRow), len(row)) + } + + for i := range row { + if row[i] != test.expectedRow[i] { + t.Errorf("expected row %v, got %v", test.expectedRow, row) + } + } + }) + } +} + +func TestClusterScan_GenerateTableNextSteps(t *testing.T) { + tests := []struct { + name string + controlSummary reportsummary.IControlSummary + expectedNextSteps string + }{ + { + name: "with id", + controlSummary: &reportsummary.ControlSummary{ + ControlID: "ctrl1", + }, + expectedNextSteps: "$ kubescape scan control ctrl1 -v", + }, { + name: "empty id", + controlSummary: &reportsummary.ControlSummary{}, + expectedNextSteps: "$ kubescape scan control -v", + }, + } + + clusterPrinter := NewClusterPrinter() + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + nextSteps := clusterPrinter.generateTableNextSteps(test.controlSummary) + + if nextSteps != test.expectedNextSteps { + t.Errorf("expected next steps %s, got %s", test.expectedNextSteps, nextSteps) + } + }) + } +} diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/datastructures.go b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/datastructures.go index 6cc3087b..28bc3e1b 100644 --- a/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/datastructures.go +++ b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/datastructures.go @@ -18,12 +18,16 @@ const ( TypeCounting CategoryType = "COUNTING" TypeStatus CategoryType = "STATUS" + // Categories to show are hardcoded by ID, so their names are not important. We also want full control over the categories and their order, so a new release of the security checks will not affect the output + + // cluster scan categories controlPlaneCategoryID = "Cat-1" accessControlCategoryID = "Cat-2" secretsCategoryID = "Cat-3" networkCategoryID = "Cat-4" workloadsCategoryID = "Cat-5" + // workload scan categories supplyChainCategoryID = "Cat-6" resourceManagementCategoryID = "Cat-7" storageCategoryID = "Cat-8" @@ -47,6 +51,7 @@ var workloadCategoriesDisplayOrder = []string{ nodeEscapeCategoryID, } +// map categories to table type. Each table type has a different display var mapCategoryToType = map[string]CategoryType{ controlPlaneCategoryID: TypeStatus, accessControlCategoryID: TypeCounting, diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/reposcan.go b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/reposcan.go index c0f08f9e..88960369 100644 --- a/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/reposcan.go +++ b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/reposcan.go @@ -36,14 +36,14 @@ func (rp *RepoPrinter) PrintCategoriesTables(writer io.Writer, summaryDetails *r continue } - infoToPrintInfo := utils.MapInfoToPrintInfoFromIface(categoryControl.controlSummaries) - - rp.renderSingleCategoryTable(categoryControl.CategoryName, mapCategoryToType[id], writer, categoryControl.controlSummaries, infoToPrintInfo) + rp.renderSingleCategoryTable(categoryControl.CategoryName, mapCategoryToType[id], writer, categoryControl.controlSummaries, utils.MapInfoToPrintInfoFromIface(categoryControl.controlSummaries)) } } func (rp *RepoPrinter) renderSingleCategoryTable(categoryName string, categoryType CategoryType, writer io.Writer, controlSummaries []reportsummary.IControlSummary, infoToPrintInfo []utils.InfoStars) { + sortControlSummaries(controlSummaries) + headers, columnAligments := initCategoryTableData(categoryType) table := getCategoryTableWriter(writer, headers, columnAligments) diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/reposcan_test.go b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/reposcan_test.go new file mode 100644 index 00000000..9e6cca39 --- /dev/null +++ b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/reposcan_test.go @@ -0,0 +1,102 @@ +package configurationprinter + +import ( + "testing" + + "github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary" +) + +func TestRepoScan_GenerateCountingCategoryRow(t *testing.T) { + tests := []struct { + name string + controlSummary reportsummary.ControlSummary + expectedRow []string + inputPatterns []string + }{ + { + name: "multiple files", + controlSummary: reportsummary.ControlSummary{ + ControlID: "ctrl1", + Name: "ctrl1", + StatusCounters: reportsummary.StatusCounters{ + FailedResources: 5, + PassedResources: 3, + SkippedResources: 2, + }, + }, + inputPatterns: []string{"file.yaml", "file2.yaml"}, + expectedRow: []string{"ctrl1", "5", "$ kubescape scan control ctrl1 file.yaml,file2.yaml"}, + }, + { + name: "one file", + controlSummary: reportsummary.ControlSummary{ + ControlID: "ctrl1", + Name: "ctrl1", + StatusCounters: reportsummary.StatusCounters{ + FailedResources: 5, + PassedResources: 3, + SkippedResources: 2, + }, + }, + inputPatterns: []string{"file.yaml"}, + expectedRow: []string{"ctrl1", "5", "$ kubescape scan control ctrl1 file.yaml"}, + }, + } + + repoPrinter := NewRepoPrinter(nil) + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + row := repoPrinter.generateCountingCategoryRow(&test.controlSummary, test.inputPatterns) + + if len(row) != len(test.expectedRow) { + t.Errorf("expected row length %d, got %d", len(test.expectedRow), len(row)) + } + + for i := range row { + if row[i] != test.expectedRow[i] { + t.Errorf("expected row %v, got %v", test.expectedRow, row) + } + } + }) + } + +} + +func TestRepoScan_GenerateTableNextSteps(t *testing.T) { + tests := []struct { + name string + controlSummary reportsummary.ControlSummary + expectedNextSteps string + inputPatterns []string + }{ + { + name: "single file", + controlSummary: reportsummary.ControlSummary{ + ControlID: "ctrl1", + }, + inputPatterns: []string{"file.yaml"}, + expectedNextSteps: "$ kubescape scan control ctrl1 file.yaml", + }, + { + name: "multiple files", + controlSummary: reportsummary.ControlSummary{ + ControlID: "ctrl1", + }, + inputPatterns: []string{"file.yaml", "file2.yaml"}, + expectedNextSteps: "$ kubescape scan control ctrl1 file.yaml,file2.yaml", + }, + } + + repoPrinter := NewRepoPrinter(nil) + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + nextSteps := repoPrinter.generateTableNextSteps(&test.controlSummary, test.inputPatterns) + + if nextSteps != test.expectedNextSteps { + t.Errorf("expected next steps %s, got %s", test.expectedNextSteps, nextSteps) + } + }) + } +} diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/utils.go b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/utils.go index 10a78b71..d237b3e6 100644 --- a/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/utils.go +++ b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/utils.go @@ -2,8 +2,12 @@ package configurationprinter import ( "fmt" + "io" + "sort" "strings" + "github.com/kubescape/kubescape/v2/core/cautils" + "github.com/kubescape/kubescape/v2/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/utils" "github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary" ) @@ -69,3 +73,15 @@ func getDocsForControl(controlSummary reportsummary.IControlSummary) string { func getRunCommandForControl(controlSummary reportsummary.IControlSummary) string { return fmt.Sprintf("%s %s -v", scanControlPrefix, controlSummary.GetID()) } + +func sortControlSummaries(controlSummaries []reportsummary.IControlSummary) { + sort.Slice(controlSummaries, func(i, j int) bool { + return controlSummaries[i].GetName() < controlSummaries[j].GetName() + }) +} + +func printCategoryInfo(writer io.Writer, infoToPrintInfo []utils.InfoStars) { + for i := range infoToPrintInfo { + cautils.InfoDisplay(writer, fmt.Sprintf("%s %s\n", infoToPrintInfo[i].Stars, infoToPrintInfo[i].Info)) + } +} diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/utils_test.go b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/utils_test.go index 9ff53559..00699df4 100644 --- a/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/utils_test.go +++ b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/utils_test.go @@ -8,7 +8,7 @@ import ( "github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary" ) -func Test_mapCategoryToSummary(t *testing.T) { +func TestMapCategoryToSummary(t *testing.T) { tests := []struct { name string @@ -329,7 +329,7 @@ func Test_mapCategoryToSummary(t *testing.T) { } } -func Test_buildCategoryToControlsMap(t *testing.T) { +func TestBuildCategoryToControlsMap(t *testing.T) { tests := []struct { name string mapCategoriesToCtrlSummary map[string][]reportsummary.ControlSummary @@ -503,7 +503,7 @@ func Test_buildCategoryToControlsMap(t *testing.T) { } } -func Test_getDocsForControl(t *testing.T) { +func TestGetDocsForControl(t *testing.T) { tests := []struct { name string controlSummary reportsummary.IControlSummary @@ -536,7 +536,7 @@ func Test_getDocsForControl(t *testing.T) { } } -func Test_getRunCommandForControl(t *testing.T) { +func TestGetRunCommandForControl(t *testing.T) { tests := []struct { name string controlSummary reportsummary.IControlSummary diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/workloadscan.go b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/workloadscan.go index 0176f48b..57f46423 100644 --- a/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/workloadscan.go +++ b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/workloadscan.go @@ -32,13 +32,13 @@ func (wp *WorkloadPrinter) PrintCategoriesTables(writer io.Writer, summaryDetail continue } - infoToPrintInfo := utils.MapInfoToPrintInfoFromIface(categoryControl.controlSummaries) - - wp.renderSingleCategoryTable(categoryControl.CategoryName, mapCategoryToType[id], writer, categoryControl.controlSummaries, infoToPrintInfo) + wp.renderSingleCategoryTable(categoryControl.CategoryName, mapCategoryToType[id], writer, categoryControl.controlSummaries, utils.MapInfoToPrintInfoFromIface(categoryControl.controlSummaries)) } } func (wp *WorkloadPrinter) renderSingleCategoryTable(categoryName string, categoryType CategoryType, writer io.Writer, controlSummaries []reportsummary.IControlSummary, infoToPrintInfo []utils.InfoStars) { + sortControlSummaries(controlSummaries) + headers, columnAligments := wp.initCategoryTableData(categoryType) table := getCategoryTableWriter(writer, headers, columnAligments) @@ -47,7 +47,7 @@ func (wp *WorkloadPrinter) renderSingleCategoryTable(categoryName string, catego for _, ctrls := range controlSummaries { var row []string if categoryType == TypeCounting { - row = wp.generateCountingCategoryRow(ctrls) + row = wp.generateCountingCategoryRow(ctrls, infoToPrintInfo) } else { row = generateCategoryStatusRow(ctrls, infoToPrintInfo) } @@ -70,13 +70,15 @@ func (wp *WorkloadPrinter) initCategoryTableData(categoryType CategoryType) ([]s return getCategoryStatusTypeHeaders(), getStatusTypeAlignments() } -func (wp *WorkloadPrinter) generateCountingCategoryRow(controlSummary reportsummary.IControlSummary) []string { +func (wp *WorkloadPrinter) generateCountingCategoryRow(controlSummary reportsummary.IControlSummary, infoToPrintInfo []utils.InfoStars) []string { - row := make([]string, 2) + row := make([]string, 3) row[0] = controlSummary.GetName() - row[1] = fmt.Sprintf("%d", controlSummary.NumberOfResources().Failed()) + row[1] = getStatus(controlSummary.GetStatus(), controlSummary, infoToPrintInfo) + + row[2] = getDocsForControl(controlSummary) return row } @@ -90,13 +92,14 @@ func (wp *WorkloadPrinter) generateNextSteps(controlSummary reportsummary.IContr } func (wp *WorkloadPrinter) getCategoryCountingTypeHeaders() []string { - headers := make([]string, 2) + headers := make([]string, 3) headers[0] = controlNameHeader - headers[1] = resourcesHeader + headers[1] = statusHeader + headers[2] = docsHeader return headers } func (wp *WorkloadPrinter) getCountingTypeAlignments() []int { - return []int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER} + return []int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_LEFT} } diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/workloadscan_test.go b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/workloadscan_test.go new file mode 100644 index 00000000..5e0850e9 --- /dev/null +++ b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter/workloadscan_test.go @@ -0,0 +1,116 @@ +package configurationprinter + +import ( + "reflect" + "testing" + + "github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary" + "github.com/olekukonko/tablewriter" + "github.com/stretchr/testify/assert" +) + +func TestWorkloadScan_InitCategoryTableData(t *testing.T) { + tests := []struct { + name string + categoryType CategoryType + expectedHeaders []string + expectedAlignments []int + }{ + { + name: "Test1", + categoryType: TypeCounting, + expectedHeaders: []string{"CONTROL NAME", "RESOURCES"}, + expectedAlignments: []int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER}, + }, + { + name: "Test2", + categoryType: TypeStatus, + expectedHeaders: []string{"CONTROL NAME", "STATUS", "DOCS"}, + expectedAlignments: []int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER}, + }, + } + + workloadPrinter := NewWorkloadPrinter() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + headers, alignments := workloadPrinter.initCategoryTableData(tt.categoryType) + if len(headers) != len(tt.expectedHeaders) { + t.Errorf("initCategoryTableData() headers = %v, want %v", headers, tt.expectedHeaders) + } + if len(alignments) != len(tt.expectedAlignments) { + t.Errorf("initCategoryTableData() alignments = %v, want %v", alignments, tt.expectedAlignments) + } + assert.True(t, reflect.DeepEqual(headers, tt.expectedHeaders)) + assert.True(t, reflect.DeepEqual(alignments, tt.expectedAlignments)) + }) + } +} + +func TestWorkloadScan_GenerateCountingCategoryRow(t *testing.T) { + tests := []struct { + name string + controlSummary reportsummary.IControlSummary + expectedRows []string + }{ + { + name: "1 failed control", + controlSummary: &reportsummary.ControlSummary{ + Name: "ctrl1", + StatusCounters: reportsummary.StatusCounters{ + FailedResources: 1, + }, + }, + expectedRows: []string{"ctrl1", "1"}, + }, + { + name: "multiple failed controls", + controlSummary: &reportsummary.ControlSummary{ + Name: "ctrl1", + StatusCounters: reportsummary.StatusCounters{ + FailedResources: 5, + }, + }, + expectedRows: []string{"ctrl1", "5"}, + }, + { + name: "no failed controls", + controlSummary: &reportsummary.ControlSummary{ + Name: "ctrl1", + StatusCounters: reportsummary.StatusCounters{ + FailedResources: 0, + }, + }, + expectedRows: []string{"ctrl1", "0"}, + }, + } + + workloadPrinter := NewWorkloadPrinter() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + row := workloadPrinter.generateCountingCategoryRow(tt.controlSummary) + assert.True(t, reflect.DeepEqual(row, tt.expectedRows)) + }) + } +} + +func TestWorkloadScan_GetCategoryCountingTypeHeaders(t *testing.T) { + + workloadPrinter := NewWorkloadPrinter() + + headers := workloadPrinter.getCategoryCountingTypeHeaders() + + assert.True(t, reflect.DeepEqual(headers, []string{"CONTROL NAME", "RESOURCES"})) + +} + +func TestWorkloadScan_GetCountingTypeAlignments(t *testing.T) { + + workloadPrinter := NewWorkloadPrinter() + + alignments := workloadPrinter.getCountingTypeAlignments() + + assert.True(t, reflect.DeepEqual(alignments, []int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER})) + +} diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/imageprinter/datastructures.go b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/imageprinter/datastructures.go index 04d734ff..cc567537 100644 --- a/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/imageprinter/datastructures.go +++ b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/imageprinter/datastructures.go @@ -22,6 +22,7 @@ type CVE struct { } type PackageScore struct { + Name string Version string Score int } diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/imageprinter/tablewriter.go b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/imageprinter/tablewriter.go index 842aba1d..b7f98cc4 100644 --- a/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/imageprinter/tablewriter.go +++ b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/imageprinter/tablewriter.go @@ -27,8 +27,5 @@ func (tw *TableWriter) PrintImageScanningTable(writer io.Writer, summary ImageSc return } - headers := getImageScanningHeaders() - columnAlignments := getImageScanningColumnsAlignments() - - renderTable(writer, headers, columnAlignments, rows) + renderTable(writer, getImageScanningHeaders(), getImageScanningColumnsAlignments(), rows) } diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/imageprinter/utils_test.go b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/imageprinter/utils_test.go index a039ba66..d5970237 100644 --- a/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/imageprinter/utils_test.go +++ b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/imageprinter/utils_test.go @@ -6,7 +6,7 @@ import ( v5 "github.com/anchore/grype/grype/db/v5" ) -func Test_generateRows(t *testing.T) { +func TestGenerateRows(t *testing.T) { test := []struct { name string summary ImageScanSummary @@ -92,7 +92,7 @@ func Test_generateRows(t *testing.T) { } } -func Test_generateRow(t *testing.T) { +func TestGenerateRow(t *testing.T) { tests := []struct { name string cve CVE @@ -136,7 +136,7 @@ func Test_generateRow(t *testing.T) { } } -func Test_getImageScanningHeaders(t *testing.T) { +func TestGetImageScanningHeaders(t *testing.T) { headers := getImageScanningHeaders() expectedHeaders := []string{"SEVERITY", "NAME", "COMPONENT", "VERSION", "FIXED IN"} diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/utils/utils_test.go b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/utils/utils_test.go new file mode 100644 index 00000000..7c3fe5d7 --- /dev/null +++ b/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/utils/utils_test.go @@ -0,0 +1,11 @@ +package utils + +import "testing" + +func TestGetColor(t *testing.T) { + +} + +func TestImageSeverityToInt(t *testing.T) { + +} diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter/utils.go b/core/pkg/resultshandling/printer/v2/prettyprinter/utils.go index 4532f29d..e21df1df 100644 --- a/core/pkg/resultshandling/printer/v2/prettyprinter/utils.go +++ b/core/pkg/resultshandling/printer/v2/prettyprinter/utils.go @@ -15,16 +15,18 @@ import ( ) const ( - linkToHelm = "https://github.com/kubescape/helm-charts" - linkToCICDSetup = "https://hub.armosec.io/docs/integrations" - complianceScanRunText = "Run a compliance scan: '$ kubescape scan framework nsa,mitre'" - clusterScanRunText = "Run a cluster scan: '$ kubescape scan'" + linkToHelm = "https://github.com/kubescape/helm-charts" + linkToCICDSetup = "https://hub.armosec.io/docs/integrations" + configScanVerboseRunText = "Run with '--verbose'/'-v' flag for detailed resources view" + imageScanVerboseRunText = "Run with '--verbose'/'-v' flag for detailed vulnerabilities view" + clusterScanRunText = "Run a cluster scan: '$ kubescape scan'" ) var ( installHelmText = fmt.Sprintf("Install helm for continuos monitoring: %s", linkToHelm) CICDSetupText = fmt.Sprintf("Add Kubescape to CICD: %s", linkToCICDSetup) complianceFrameworks = []string{"nsa", "mitre"} + cveSeverities = []string{"Critical", "High", "Medium", "Low", "Negligible", "Unknown"} ) func filterComplianceFrameworks(frameworks []reportsummary.IFrameworkSummary) []reportsummary.IFrameworkSummary { @@ -59,10 +61,10 @@ func getTopWorkloadsTitle(topWLsLen int) string { } // getSeverityToSummaryMap returns a map of severity to summary, if shouldMerge is true, it will merge Low, Negligible and Unknown to Other -func getSeverityToSummaryMap(summary imageprinter.ImageScanSummary, shouldMerge bool) map[string]*imageprinter.SeveritySummary { +func getSeverityToSummaryMap(summary imageprinter.ImageScanSummary, verboseMode bool) map[string]*imageprinter.SeveritySummary { tempMap := map[string]*imageprinter.SeveritySummary{} for severity, severitySummary := range summary.MapsSeverityToSummary { - if shouldMerge { + if !verboseMode { if severity == "Low" || severity == "Negligible" || severity == "Unknown" { severity = "Other" } @@ -73,9 +75,28 @@ func getSeverityToSummaryMap(summary imageprinter.ImageScanSummary, shouldMerge tempMap[severity].NumberOfCVEs += severitySummary.NumberOfCVEs tempMap[severity].NumberOfFixableCVEs += severitySummary.NumberOfFixableCVEs } + + addEmptySeverities(tempMap, verboseMode) + return tempMap } +func addEmptySeverities(mapSeverityTSummary map[string]*imageprinter.SeveritySummary, verboseMode bool) { + if verboseMode { + for _, severity := range cveSeverities { + if _, ok := mapSeverityTSummary[severity]; !ok { + mapSeverityTSummary[severity] = &imageprinter.SeveritySummary{} + } + } + } else { + for _, severity := range []string{"Critical", "High"} { + if _, ok := mapSeverityTSummary[severity]; !ok { + mapSeverityTSummary[severity] = &imageprinter.SeveritySummary{} + } + } + } +} + // filterCVEsBySeverities returns a list of CVEs only with the severities that are in the severities list func filterCVEsBySeverities(cves []imageprinter.CVE, severities []string) []imageprinter.CVE { var filteredCVEs []imageprinter.CVE @@ -123,8 +144,8 @@ func printTopVulnerabilities(writer *os.File, summary imageprinter.ImageScanSumm cautils.InfoTextDisplay(writer, "\nMost vulnerable components:\n") topVulnerablePackages := sortTopVulnerablePackages(summary.PackageScores) - for k, v := range topVulnerablePackages { - cautils.SimpleDisplay(writer, " * %s (%s)\n", k, v.Version) + for _, v := range topVulnerablePackages { + cautils.SimpleDisplay(writer, " * %s (%s)\n", v.Name, v.Version) } cautils.SimpleDisplay(writer, "\n") @@ -133,7 +154,7 @@ func printTopVulnerabilities(writer *os.File, summary imageprinter.ImageScanSumm } func printImageScanningSummary(writer *os.File, summary imageprinter.ImageScanSummary, verboseMode bool) { - mapSeverityTSummary := getSeverityToSummaryMap(summary, !verboseMode) + mapSeverityTSummary := getSeverityToSummaryMap(summary, verboseMode) // sort keys by severity keys := make([]string, 0, len(mapSeverityTSummary)) @@ -144,7 +165,12 @@ func printImageScanningSummary(writer *os.File, summary imageprinter.ImageScanSu return utils.ImageSeverityToInt(keys[i]) > utils.ImageSeverityToInt(keys[j]) }) - cautils.InfoTextDisplay(writer, "Summary - %d vulnerabilities found:\n", len(summary.CVEs)) + if len(summary.CVEs) == 0 { + cautils.InfoTextDisplay(writer, "Vulnerability summary - no vulnerabilities were found!\n\n") + return + } + + cautils.InfoTextDisplay(writer, "Vulnerability summary - %d vulnerabilities found:\n", len(summary.CVEs)) for _, k := range keys { if k == "Other" { @@ -154,7 +180,17 @@ func printImageScanningSummary(writer *os.File, summary imageprinter.ImageScanSu } } - cautils.SimpleDisplay(writer, "\n") +} + +func printImagesCommands(writer *os.File, summary imageprinter.ImageScanSummary) { + cautils.SimpleDisplay(writer, "(Scanned images: %s)\n", strings.Join(summary.Images, ", ")) + + for _, img := range summary.Images { + imgWithoutTag := strings.Split(img, ":")[0] + cautils.SimpleDisplay(writer, fmt.Sprintf("Receive full report for %s image by running: '$ kubescape scan image %s'\n", imgWithoutTag, img)) + } + + cautils.InfoTextDisplay(writer, "\n") } func printNextSteps(writer *os.File, nextSteps []string) { @@ -162,7 +198,6 @@ func printNextSteps(writer *os.File, nextSteps []string) { for _, ns := range nextSteps { cautils.SimpleDisplay(writer, "- "+ns+"\n") } - cautils.SimpleDisplay(writer, "\n") } func printComplianceScore(writer *os.File, frameworks []reportsummary.IFrameworkSummary) { @@ -171,7 +206,7 @@ func printComplianceScore(writer *os.File, frameworks []reportsummary.IFramework cautils.SimpleDisplay(writer, "* %s: %.2f%%\n", fw.GetName(), fw.GetComplianceScore()) } - cautils.SimpleDisplay(writer, "View full compliance report by running: $ kubescape scan framework nsa,mitre\n") + cautils.SimpleDisplay(writer, "View full compliance report by running:'$ kubescape scan framework nsa,mitre'\n") cautils.InfoTextDisplay(writer, "\n") } diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter/workloadscan.go b/core/pkg/resultshandling/printer/v2/prettyprinter/workloadscan.go index a70ace1f..cf63c5c2 100644 --- a/core/pkg/resultshandling/printer/v2/prettyprinter/workloadscan.go +++ b/core/pkg/resultshandling/printer/v2/prettyprinter/workloadscan.go @@ -1,10 +1,8 @@ package prettyprinter import ( - "fmt" "os" - "github.com/kubescape/kubescape/v2/core/cautils" "github.com/kubescape/kubescape/v2/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/configurationprinter" "github.com/kubescape/kubescape/v2/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/imageprinter" "github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary" @@ -25,18 +23,8 @@ func NewWorkloadPrinter(writer *os.File) *WorkloadPrinter { var _ MainPrinter = &WorkloadPrinter{} func (wp *WorkloadPrinter) PrintImageScanning(summary *imageprinter.ImageScanSummary) { - wp.printImageScanningSummary(summary) -} - -func (wp *WorkloadPrinter) printImageScanningSummary(summary *imageprinter.ImageScanSummary) { printImageScanningSummary(wp.writer, *summary, false) - - for _, img := range summary.Images { - cautils.SimpleDisplay(wp.writer, fmt.Sprintf("Receive full report by running: 'kubescape scan image %s'\n", img)) - } - - cautils.InfoTextDisplay(wp.writer, "\n") - + printImagesCommands(wp.writer, *summary) } func (wp *WorkloadPrinter) PrintNextSteps() { @@ -45,6 +33,7 @@ func (wp *WorkloadPrinter) PrintNextSteps() { func (wp *WorkloadPrinter) getNextSteps() []string { return []string{ + configScanVerboseRunText, installHelmText, CICDSetupText, } diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter_test.go b/core/pkg/resultshandling/printer/v2/prettyprinter_test.go new file mode 100644 index 00000000..b0c0dd81 --- /dev/null +++ b/core/pkg/resultshandling/printer/v2/prettyprinter_test.go @@ -0,0 +1 @@ +package printer diff --git a/core/pkg/resultshandling/printer/v2/utils.go b/core/pkg/resultshandling/printer/v2/utils.go index ae8c013b..0ea24f10 100644 --- a/core/pkg/resultshandling/printer/v2/utils.go +++ b/core/pkg/resultshandling/printer/v2/utils.go @@ -87,38 +87,14 @@ func finalizeResources(results []resourcesresults.Result, allResources map[strin return resources } -func insertSeveritiesSummariesIntoMap(mapSeverityToSummary map[string]*imageprinter.SeveritySummary, imageScanSummary imageprinter.ImageScanSummary) { - for k, v := range mapSeverityToSummary { - severitySummary, ok := imageScanSummary.MapsSeverityToSummary[k] - if !ok { - imageScanSummary.MapsSeverityToSummary[k] = v - continue - } - severitySummary.NumberOfCVEs = severitySummary.NumberOfCVEs + v.NumberOfCVEs - severitySummary.NumberOfFixableCVEs = severitySummary.NumberOfFixableCVEs + v.NumberOfFixableCVEs - imageScanSummary.MapsSeverityToSummary[k] = severitySummary - } -} - -func insertPackageScoresIntoMap(mapPackageNameToScore map[string]*imageprinter.PackageScore, imageScanSummary imageprinter.ImageScanSummary) { - for k, v := range mapPackageNameToScore { - pkgScore, ok := imageScanSummary.PackageScores[k] - if !ok { - imageScanSummary.PackageScores[k] = v - continue - } - pkgScore.Score = pkgScore.Score + v.Score - imageScanSummary.PackageScores[k] = pkgScore - } -} - +// returns map of severity to summary func extractSeverityToSummaryMap(cves []imageprinter.CVE) map[string]*imageprinter.SeveritySummary { mapSeverityToSummary := map[string]*imageprinter.SeveritySummary{} for _, cve := range cves { if _, ok := mapSeverityToSummary[cve.Severity]; !ok { mapSeverityToSummary[cve.Severity] = &imageprinter.SeveritySummary{} } - mapSeverityToSummary[cve.Severity].NumberOfCVEs = mapSeverityToSummary[cve.Severity].NumberOfCVEs + 1 + mapSeverityToSummary[cve.Severity].NumberOfCVEs += 1 if cve.FixedState == string(v5.FixedState) { mapSeverityToSummary[cve.Severity].NumberOfFixableCVEs = mapSeverityToSummary[cve.Severity].NumberOfFixableCVEs + 1 } @@ -126,32 +102,34 @@ func extractSeverityToSummaryMap(cves []imageprinter.CVE) map[string]*imageprint return mapSeverityToSummary } -func extractPkgNameToScore(doc models.Document) map[string]*imageprinter.PackageScore { +// returns a map of package name + version to score (we can have multiple matches for the same package with different versions) +func extractPkgNameToScoreMap(matches []models.Match) map[string]*imageprinter.PackageScore { mapPackageNameToScore := make(map[string]*imageprinter.PackageScore, 0) - for _, cve := range doc.Matches { - if _, ok := mapPackageNameToScore[cve.Artifact.Name]; !ok { - mapPackageNameToScore[cve.Artifact.Name] = &imageprinter.PackageScore{ - Score: 0, + for i := range matches { + key := matches[i].Artifact.Name + matches[i].Artifact.Version + if _, ok := mapPackageNameToScore[key]; !ok { + mapPackageNameToScore[key] = &imageprinter.PackageScore{ + Version: matches[i].Artifact.Version, + Name: matches[i].Artifact.Name, } } - mapPackageNameToScore[cve.Artifact.Name].Score = mapPackageNameToScore[cve.Artifact.Name].Score + utils.ImageSeverityToInt(cve.Vulnerability.Severity) - mapPackageNameToScore[cve.Artifact.Name].Version = cve.Artifact.Version + mapPackageNameToScore[key].Score = mapPackageNameToScore[key].Score + utils.ImageSeverityToInt(matches[i].Vulnerability.Severity) } return mapPackageNameToScore } -func extractCVEs(doc models.Document) []imageprinter.CVE { - cves := []imageprinter.CVE{} - for _, match := range doc.Matches { +func extractCVEs(matches []models.Match) []imageprinter.CVE { + CVEs := []imageprinter.CVE{} + for i := range matches { cve := imageprinter.CVE{ - ID: match.Vulnerability.ID, - Severity: match.Vulnerability.Severity, - Package: match.Artifact.Name, - Version: match.Artifact.Version, - FixVersions: match.Vulnerability.Fix.Versions, - FixedState: match.Vulnerability.Fix.State, + ID: matches[i].Vulnerability.ID, + Severity: matches[i].Vulnerability.Severity, + Package: matches[i].Artifact.Name, + Version: matches[i].Artifact.Version, + FixVersions: matches[i].Vulnerability.Fix.Versions, + FixedState: matches[i].Vulnerability.Fix.State, } - cves = append(cves, cve) + CVEs = append(CVEs, cve) } - return cves + return CVEs } diff --git a/core/pkg/resultshandling/printer/v2/utils_test.go b/core/pkg/resultshandling/printer/v2/utils_test.go new file mode 100644 index 00000000..9bd28316 --- /dev/null +++ b/core/pkg/resultshandling/printer/v2/utils_test.go @@ -0,0 +1,480 @@ +package printer + +import ( + "testing" + + "github.com/anchore/grype/grype/presenter/models" + "github.com/kubescape/kubescape/v2/core/pkg/resultshandling/printer/v2/prettyprinter/tableprinter/imageprinter" + "github.com/stretchr/testify/assert" +) + +func TestExtractCVEs(t *testing.T) { + tests := []struct { + name string + matches []models.Match + want []imageprinter.CVE + }{ + { + name: "single vuln", + matches: []models.Match{ + { + Artifact: models.Package{ + Name: "foo", + Version: "1.2.3", + }, + Vulnerability: models.Vulnerability{ + VulnerabilityMetadata: models.VulnerabilityMetadata{ + ID: "CVE-2020-1234", + Severity: "High", + }, + Fix: models.Fix{ + Versions: []string{"1.2.3"}, + State: "Fixed", + }, + }, + }, + }, + want: []imageprinter.CVE{ + { + ID: "CVE-2020-1234", + Severity: "High", + Package: "foo", + Version: "1.2.3", + FixVersions: []string{"1.2.3"}, + FixedState: "Fixed", + }, + }, + }, + { + name: "multiple vulns", + matches: []models.Match{ + { + Artifact: models.Package{ + Name: "foo", + Version: "1.2.3", + }, + Vulnerability: models.Vulnerability{ + VulnerabilityMetadata: models.VulnerabilityMetadata{ + ID: "CVE-2020-1234", + Severity: "High", + }, + Fix: models.Fix{ + Versions: []string{"1.2.3"}, + State: "Fixed", + }, + }, + }, + { + Artifact: models.Package{ + Name: "test", + Version: "1", + }, + Vulnerability: models.Vulnerability{ + VulnerabilityMetadata: models.VulnerabilityMetadata{ + ID: "CVE-2020-1235", + Severity: "Critical", + }, + Fix: models.Fix{ + Versions: []string{"1"}, + State: "Fixed", + }, + }, + }, + { + Artifact: models.Package{ + Name: "test2", + Version: "3", + }, + Vulnerability: models.Vulnerability{ + VulnerabilityMetadata: models.VulnerabilityMetadata{ + ID: "CVE-2020-1236", + Severity: "Low", + }, + Fix: models.Fix{ + Versions: []string{"2", "3", "4"}, + State: "Not fixed", + }, + }, + }, + }, + want: []imageprinter.CVE{ + { + ID: "CVE-2020-1234", + Severity: "High", + Package: "foo", + Version: "1.2.3", + FixVersions: []string{"1.2.3"}, + FixedState: "Fixed", + }, + { + ID: "CVE-2020-1235", + Severity: "Critical", + Package: "test", + Version: "1", + FixVersions: []string{"1"}, + FixedState: "Fixed", + }, + { + ID: "CVE-2020-1236", + Severity: "Low", + Package: "test2", + Version: "3", + FixVersions: []string{"2", "3", "4"}, + FixedState: "Not fixed", + }, + }, + }, + { + name: "empty vulns", + matches: []models.Match{}, + want: []imageprinter.CVE{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := extractCVEs(tt.matches) + if len(actual) != len(tt.want) { + t.Errorf("extractCVEs() = %v, want %v", actual, tt.want) + } + for i := range actual { + if actual[i].ID != tt.want[i].ID { + t.Errorf("extractCVEs() = %v, want %v", actual, tt.want) + } + if actual[i].Severity != tt.want[i].Severity { + t.Errorf("extractCVEs() = %v, want %v", actual, tt.want) + } + if actual[i].Package != tt.want[i].Package { + t.Errorf("extractCVEs() = %v, want %v", actual, tt.want) + } + if actual[i].Version != tt.want[i].Version { + t.Errorf("extractCVEs() = %v, want %v", actual, tt.want) + } + if actual[i].FixedState != tt.want[i].FixedState { + t.Errorf("extractCVEs() = %v, want %v", actual, tt.want) + } + if len(actual[i].FixVersions) != len(tt.want[i].FixVersions) { + t.Errorf("extractCVEs() = %v, want %v", actual, tt.want) + } + for j := range actual[i].FixVersions { + if actual[i].FixVersions[j] != tt.want[i].FixVersions[j] { + t.Errorf("extractCVEs() = %v, want %v", actual, tt.want) + } + } + } + }) + } + +} + +func TestExtractPkgNameToScoreMap(t *testing.T) { + tests := []struct { + name string + matches []models.Match + want map[string]*imageprinter.PackageScore + }{ + { + name: "single package", + matches: []models.Match{ + { + Artifact: models.Package{ + Name: "foo", + Version: "1.2.3", + }, + Vulnerability: models.Vulnerability{ + VulnerabilityMetadata: models.VulnerabilityMetadata{ + Severity: "High", + }, + }, + }, + }, + want: map[string]*imageprinter.PackageScore{ + "foo1.2.3": { + Name: "foo", + Score: 4, + Version: "1.2.3", + }, + }, + }, + { + name: "multiple packages - different versions", + matches: []models.Match{ + { + Artifact: models.Package{ + Name: "pkg1", + Version: "version1", + }, + Vulnerability: models.Vulnerability{ + VulnerabilityMetadata: models.VulnerabilityMetadata{ + Severity: "Critical", + }, + }, + }, + { + Artifact: models.Package{ + Name: "pkg2", + Version: "1.2", + }, + Vulnerability: models.Vulnerability{ + VulnerabilityMetadata: models.VulnerabilityMetadata{ + Severity: "Low", + }, + }, + }, + { + Artifact: models.Package{ + Name: "pkg3", + Version: "1.2.3", + }, + Vulnerability: models.Vulnerability{ + VulnerabilityMetadata: models.VulnerabilityMetadata{ + Severity: "High", + }, + }, + }, + }, + want: map[string]*imageprinter.PackageScore{ + "pkg1version1": { + Name: "pkg1", + Score: 5, + Version: "version1", + }, + "pkg21.2": { + Name: "pkg2", + Score: 2, + Version: "1.2", + }, + "pkg31.2.3": { + Name: "pkg3", + Score: 4, + Version: "1.2.3", + }, + }, + }, + { + name: "multiple packages - mixed versions", + matches: []models.Match{ + { + Artifact: models.Package{ + Name: "pkg1", + Version: "version1", + }, + Vulnerability: models.Vulnerability{ + VulnerabilityMetadata: models.VulnerabilityMetadata{ + Severity: "High", + }, + }, + }, + { + Artifact: models.Package{ + Name: "pkg1", + Version: "version1", + }, + Vulnerability: models.Vulnerability{ + VulnerabilityMetadata: models.VulnerabilityMetadata{ + Severity: "High", + }, + }, + }, + { + Artifact: models.Package{ + Name: "pkg1", + Version: "version2", + }, + Vulnerability: models.Vulnerability{ + VulnerabilityMetadata: models.VulnerabilityMetadata{ + Severity: "Critical", + }, + }, + }, + { + Artifact: models.Package{ + Name: "pkg3", + Version: "1.2", + }, + Vulnerability: models.Vulnerability{ + VulnerabilityMetadata: models.VulnerabilityMetadata{ + Severity: "Medium", + }, + }, + }, + { + Artifact: models.Package{ + Name: "pkg3", + Version: "1.2", + }, + Vulnerability: models.Vulnerability{ + VulnerabilityMetadata: models.VulnerabilityMetadata{ + Severity: "Low", + }, + }, + }, + { + Artifact: models.Package{ + Name: "pkg4", + Version: "1.2.3", + }, + Vulnerability: models.Vulnerability{ + VulnerabilityMetadata: models.VulnerabilityMetadata{ + Severity: "High", + }, + }, + }, + }, + want: map[string]*imageprinter.PackageScore{ + "pkg1version1": { + Name: "pkg1", + Score: 8, + Version: "version1", + }, + "pkg1version2": { + Name: "pkg1", + Score: 5, + Version: "version2", + }, + "pkg31.2": { + Name: "pkg3", + Score: 5, + Version: "1.2", + }, + "pkg41.2.3": { + Name: "pkg4", + Score: 4, + Version: "1.2.3", + }, + }, + }, + { + name: "empty packages", + matches: []models.Match{}, + want: map[string]*imageprinter.PackageScore{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := extractPkgNameToScoreMap(tt.matches) + if len(actual) == 0 { + assert.Equal(t, tt.want, actual) + return + } + + if len(actual) != len(tt.want) { + t.Errorf("extractPkgNameToScore() = %v, want %v", actual, tt.want) + } + + for k := range actual { + if actual[k].Score != tt.want[k].Score { + t.Errorf("extractPkgNameToScore() = %v, want %v", actual, tt.want) + } + if actual[k].Version != tt.want[k].Version { + t.Errorf("extractPkgNameToScore() = %v, want %v", actual, tt.want) + } + if actual[k].Name != tt.want[k].Name { + t.Errorf("extractPkgNameToScore() = %v, want %v", actual, tt.want) + } + } + }) + } +} + +// func TestExtractSeverityToSummaryMap(t *testing.T) { +// tests := []struct { +// name string +// cves []imageprinter.CVE +// want map[string]*imageprinter.SeveritySummary +// }{ +// { +// name: "single cve", +// cves: []imageprinter.CVE{ +// { +// ID: "CVE-2020-1234", +// Severity: "High", +// FixedState: string(v5.FixedState), +// }, +// }, +// want: map[string]*imageprinter.SeveritySummary{ +// "High": { +// NumberOfCVEs: 1, +// NumberOfFixableCVEs: 1, +// }, +// }, +// }, +// { +// name: "multiple cves", +// cves: []imageprinter.CVE{ +// { +// ID: "CVE-2020-1234", +// Severity: "High", +// FixedState: string(v5.FixedState), +// }, +// { +// ID: "CVE-2020-1235", +// Severity: "High", +// FixedState: string(v5.NotFixedState), +// }, +// { +// ID: "CVE-2020-23", +// Severity: "Low", +// FixedState: string(v5.NotFixedState), +// }, +// { +// ID: "CVE-2020-4321", +// Severity: "Medium", +// FixedState: string(v5.NotFixedState), +// }, +// { +// ID: "CVE-2020-53152", +// Severity: "Negligible", +// FixedState: string(v5.NotFixedState), +// }, +// { +// ID: "CVE-2020-531524", +// Severity: "Negligible", +// FixedState: string(v5.NotFixedState), +// }, +// }, +// want: map[string]*imageprinter.SeveritySummary{ +// "High": { +// NumberOfCVEs: 2, +// NumberOfFixableCVEs: 1, +// }, +// "Low": { +// NumberOfCVEs: 1, +// NumberOfFixableCVEs: 0, +// }, +// "Medium": { +// NumberOfCVEs: 1, +// NumberOfFixableCVEs: 0, +// }, +// "Negligible": { +// NumberOfCVEs: 2, +// NumberOfFixableCVEs: 0, +// }, +// }, +// }, +// } + +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// actual := extractSeverityToSummaryMap(tt.cves) +// if len(actual) == 0 { +// assert.Equal(t, tt.want, actual) +// return +// } + +// if len(actual) != len(tt.want) { +// t.Errorf("extractSeverityToSummaryMap() = %v, want %v", actual, tt.want) +// } + +// for k := range actual { +// if actual[k].NumberOfCVEs != tt.want[k].NumberOfCVEs { +// t.Errorf("extractSeverityToSummaryMap() = %v, want %v", actual, tt.want) +// } +// if actual[k].NumberOfFixableCVEs != tt.want[k].NumberOfFixableCVEs { +// t.Errorf("extractSeverityToSummaryMap() = %v, want %v", actual, tt.want) +// } +// } +// }) +// } +// }