diff --git a/cmd/scan/framework.go b/cmd/scan/framework.go index 6cb508b8..81323ba1 100644 --- a/cmd/scan/framework.go +++ b/cmd/scan/framework.go @@ -89,6 +89,7 @@ func getFrameworkCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Comm } if len(args) > 1 { if len(args[1:]) == 0 || args[1] != "-" { + scanInfo.ScanType = cautils.ScanTypeRepo scanInfo.InputPatterns = args[1:] logger.L().Debug("List of input files", helpers.Interface("patterns", scanInfo.InputPatterns)) } else { // store stdin to file - do NOT move to separate function !! diff --git a/core/cautils/datastructures.go b/core/cautils/datastructures.go index 1a1a40ca..6bf9862a 100644 --- a/core/cautils/datastructures.go +++ b/core/cautils/datastructures.go @@ -9,6 +9,7 @@ import ( apis "github.com/kubescape/opa-utils/reporthandling/apis" "github.com/kubescape/opa-utils/reporthandling/attacktrack/v1alpha1" "github.com/kubescape/opa-utils/reporthandling/results/v1/prioritization" + "github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary" "github.com/kubescape/opa-utils/reporthandling/results/v1/resourcesresults" reporthandlingv2 "github.com/kubescape/opa-utils/reporthandling/v2" ) @@ -20,7 +21,9 @@ type KSResources map[string][]string type ScanTypes string const ( - ScanTypeCluster ScanTypes = "cluster" + TopWorkloadsNumber = 5 + ScanTypeCluster ScanTypes = "cluster" + ScanTypeRepo ScanTypes = "repo" ) type OPASessionObj struct { @@ -62,6 +65,22 @@ func NewOPASessionObj(ctx context.Context, frameworks []reporthandling.Framework } } +func (sessionObj *OPASessionObj) SetTopWorkloads() { + count := 0 + + for key := range sessionObj.ResourcesPrioritized { + if count >= TopWorkloadsNumber { + break + } + wlObj := reportsummary.TopWorkload{ + Workload: sessionObj.AllResources[key], + ResourceSource: sessionObj.ResourceSource[key], + } + sessionObj.Report.SummaryDetails.TopWorkloadsByScore = append(sessionObj.Report.SummaryDetails.TopWorkloadsByScore, wlObj) + count++ + } +} + func (sessionObj *OPASessionObj) SetMapNamespaceToNumberOfResources(mapNamespaceToNumberOfResources map[string]int) { if sessionObj.Metadata.ContextMetadata.ClusterContextMetadata == nil { sessionObj.Metadata.ContextMetadata.ClusterContextMetadata = &reporthandlingv2.ClusterMetadata{} diff --git a/core/cautils/getter/datastructures.go b/core/cautils/getter/datastructures.go index 365cf126..11b6b363 100644 --- a/core/cautils/getter/datastructures.go +++ b/core/cautils/getter/datastructures.go @@ -8,7 +8,7 @@ import ( ) // NativeFrameworks identifies all pre-built, native frameworks. -var NativeFrameworks = []string{"security"} +var NativeFrameworks = []string{"security", "mitre", "nsa"} type ( // TenantResponse holds the credentials for a tenant. diff --git a/core/core/initutils.go b/core/core/initutils.go index 95c83e25..92a7dee8 100644 --- a/core/core/initutils.go +++ b/core/core/initutils.go @@ -286,7 +286,7 @@ func getAttackTracksGetter(ctx context.Context, attackTracks, accountID string, } // getUIPrinter returns a printer that will be used to print to the program’s UI (terminal) -func getUIPrinter(ctx context.Context, verboseMode bool, formatVersion string, attackTree bool, viewType cautils.ViewTypes, scanType cautils.ScanTypes) printer.IPrinter { +func getUIPrinter(ctx context.Context, verboseMode bool, formatVersion string, attackTree bool, viewType cautils.ViewTypes, scanType cautils.ScanTypes, scanContext cautils.ScanningContext) printer.IPrinter { var p printer.IPrinter if helpers.ToLevel(logger.L().GetLevel()) >= helpers.WarningLevel { p = &printerv2.SilentPrinter{} diff --git a/core/core/scan.go b/core/core/scan.go index cacda531..b8095451 100644 --- a/core/core/scan.go +++ b/core/core/scan.go @@ -103,8 +103,7 @@ func getInterfaces(ctx context.Context, scanInfo *cautils.ScanInfo) componentInt printerHandler.SetWriter(ctx, scanInfo.Output) outputPrinters = append(outputPrinters, printerHandler) } - - uiPrinter := getUIPrinter(ctx, scanInfo.VerboseMode, scanInfo.FormatVersion, scanInfo.PrintAttackTree, cautils.ViewTypes(scanInfo.View), cautils.ScanTypes(scanInfo.ScanType)) + uiPrinter := getUIPrinter(ctx, scanInfo.VerboseMode, scanInfo.FormatVersion, scanInfo.PrintAttackTree, cautils.ViewTypes(scanInfo.View), scanInfo.ScanType, scanInfo.GetScanningContext()) // ================== return interface ====================================== @@ -178,13 +177,17 @@ func (ks *Kubescape) Scan(ctx context.Context, scanInfo *cautils.ScanInfo) (*res } // ======================== prioritization =================== - if scanInfo.PrintAttackTree { + scanInfo.ScanType = cautils.ScanTypeRepo + if scanInfo.PrintAttackTree || isPrioritizationScanType(scanInfo.ScanType) { _, spanPrioritization := otel.Tracer("").Start(ctxOpa, "prioritization") if priotizationHandler, err := resourcesprioritization.NewResourcesPrioritizationHandler(ctxOpa, scanInfo.Getters.AttackTracksGetter, scanInfo.PrintAttackTree); err != nil { logger.L().Ctx(ctx).Warning("failed to get attack tracks, this may affect the scanning results", helpers.Error(err)) } else if err := priotizationHandler.PrioritizeResources(scanData); err != nil { return resultsHandling, fmt.Errorf("%w", err) } + if err == nil && isPrioritizationScanType(scanInfo.ScanType) { + scanData.SetTopWorkloads() + } spanPrioritization.End() } @@ -197,3 +200,7 @@ func (ks *Kubescape) Scan(ctx context.Context, scanInfo *cautils.ScanInfo) (*res return resultsHandling, nil } + +func isPrioritizationScanType(scanType cautils.ScanTypes) bool { + return scanType == cautils.ScanTypeCluster || scanType == cautils.ScanTypeRepo +} diff --git a/core/pkg/resultshandling/printer/v2/categorietable.go b/core/pkg/resultshandling/printer/v2/categorietable.go deleted file mode 100644 index f1c3a071..00000000 --- a/core/pkg/resultshandling/printer/v2/categorietable.go +++ /dev/null @@ -1,78 +0,0 @@ -package printer - -import ( - "fmt" - "os" - - "github.com/kubescape/kubescape/v2/core/cautils" - "github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary" - "github.com/olekukonko/tablewriter" -) - -const ( - categoriesColumnSeverity = iota - categoriesColumnName = iota - categoriesColumnFailed = iota - categoriesColumnNextSteps = iota -) - -var ( - mapScanTypeToOutput = map[cautils.ScanTypes]string{ - cautils.ScanTypeCluster: "Security Overview", - } -) - -func generateCategoriesRow(controlSummary reportsummary.IControlSummary) []string { - row := make([]string, 4) - row[categoriesColumnSeverity] = getSeverityColumn(controlSummary) - - if len(controlSummary.GetName()) > 50 { - row[categoriesColumnName] = controlSummary.GetName()[:50] + "..." - } else { - row[categoriesColumnName] = controlSummary.GetName() - } - - row[categoriesColumnFailed] = fmt.Sprintf("%d", controlSummary.NumberOfResources().Failed()) - row[categoriesColumnNextSteps] = generateNextSteps(controlSummary) - - return row -} - -func generateNextSteps(controlSummary reportsummary.IControlSummary) string { - return fmt.Sprintf("kubescape scan control %s", controlSummary.GetID()) -} - -func getCategoriesTableHeaders() []string { - headers := make([]string, 4) - headers[categoriesColumnSeverity] = "SEVERITY" - headers[categoriesColumnName] = "CONTROL NAME" - headers[categoriesColumnFailed] = "FAILED RESOURCES" - headers[categoriesColumnNextSteps] = "NEXT STEPS" - - return headers -} - -func getCategoriesColumnsAlignments() []int { - alignments := make([]int, 4) - alignments[categoriesColumnSeverity] = tablewriter.ALIGN_LEFT - alignments[categoriesColumnName] = tablewriter.ALIGN_LEFT - alignments[categoriesColumnFailed] = tablewriter.ALIGN_CENTER - alignments[categoriesColumnNextSteps] = tablewriter.ALIGN_LEFT - - return alignments -} - -func renderSingleCategory(writer *os.File, category string, ctrls []reportsummary.IControlSummary, categoriesTable *tablewriter.Table) { - cautils.InfoTextDisplay(writer, "\n"+category+"\n") - - var rows [][]string - for i := range ctrls { - row := generateCategoriesRow(ctrls[i]) - if len(row) > 0 { - rows = append(rows, row) - } - } - categoriesTable.ClearRows() - categoriesTable.AppendBulk(rows) - categoriesTable.Render() -} diff --git a/core/pkg/resultshandling/printer/v2/clusterprinter.go b/core/pkg/resultshandling/printer/v2/clusterprinter.go deleted file mode 100644 index 22e9186b..00000000 --- a/core/pkg/resultshandling/printer/v2/clusterprinter.go +++ /dev/null @@ -1,32 +0,0 @@ -package printer - -import ( - "fmt" - "os" - - "github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary" - "github.com/olekukonko/tablewriter" -) - -type clusterPrinter struct { -} - -var _ mainPrinter = &clusterPrinter{} - -func NewClusterPrinter() *clusterPrinter { - return &clusterPrinter{} -} - -func (cp *clusterPrinter) PrintMainTable(writer *os.File, summaryDetails *reportsummary.SummaryDetails, sortedControlIDs [][]string) { - categoriesToControlSummariesMap := mapCategoryToControlSummaries(*summaryDetails) - - categoriesTable := tablewriter.NewWriter(writer) - categoriesTable.SetHeader(getCategoriesTableHeaders()) - categoriesTable.SetHeaderLine(true) - categoriesTable.SetColumnAlignment(getCategoriesColumnsAlignments()) - - for category, ctrls := range categoriesToControlSummariesMap { - renderSingleCategory(writer, category, ctrls, categoriesTable) - } - fmt.Println("") -} diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter.go b/core/pkg/resultshandling/printer/v2/prettyprinter.go index 22c32b36..f8e095d8 100644 --- a/core/pkg/resultshandling/printer/v2/prettyprinter.go +++ b/core/pkg/resultshandling/printer/v2/prettyprinter.go @@ -12,11 +12,10 @@ import ( "github.com/kubescape/k8s-interface/workloadinterface" "github.com/kubescape/kubescape/v2/core/cautils" "github.com/kubescape/kubescape/v2/core/pkg/resultshandling/printer" + "github.com/kubescape/kubescape/v2/core/pkg/resultshandling/printer/v2/prettyprinter" "github.com/kubescape/opa-utils/objectsenvelopes" "github.com/kubescape/opa-utils/reporthandling/apis" - helpersv1 "github.com/kubescape/opa-utils/reporthandling/helpers/v1" "github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary" - "github.com/olekukonko/tablewriter" ) const ( @@ -33,20 +32,17 @@ type PrettyPrinter struct { verboseMode bool printAttackTree bool scanType cautils.ScanTypes - mainPrinter mainPrinter -} - -type mainPrinter interface { - PrintMainTable(writer *os.File, summaryDetails *reportsummary.SummaryDetails, sortedControlIDs [][]string) + mainPrinter prettyprinter.MainPrinter } func NewPrettyPrinter(verboseMode bool, formatVersion string, attackTree bool, viewType cautils.ViewTypes, scanType cautils.ScanTypes) *PrettyPrinter { + return &PrettyPrinter{ verboseMode: verboseMode, formatVersion: formatVersion, viewType: viewType, printAttackTree: attackTree, - scanType: scanType, + mainPrinter: prettyprinter.NewMainPrinter(scanType), } } @@ -64,17 +60,7 @@ func (pp *PrettyPrinter) ActionPrint(_ context.Context, opaSessionObj *cautils.O } } - if pp.scanType != "" { - cautils.InfoTextDisplay(pp.writer, "\n"+mapScanTypeToOutput[pp.scanType]+":\n") - } - - pp.mainPrinter.PrintMainTable(pp.writer, &opaSessionObj.Report.SummaryDetails, sortedControlIDs) - - if pp.scanType == cautils.ScanTypeCluster { - pp.printCategoriesTables(&opaSessionObj.Report.SummaryDetails) - } else { - pp.printSummaryTable(&opaSessionObj.Report.SummaryDetails, sortedControlIDs) - } + pp.mainPrinter.Print(pp.writer, &opaSessionObj.Report.SummaryDetails, sortedControlIDs) // When writing to Stdout, we aren’t really writing to an output file, // so no need to print that we are @@ -216,83 +202,6 @@ func generateRelatedObjectsStr(workload WorkloadSummary) string { } return relatedStr } -func generateFooter(summaryDetails *reportsummary.SummaryDetails) []string { - // Severity | Control name | failed resources | all resources | % success - row := make([]string, _rowLen) - row[columnName] = "Resource Summary" - row[columnCounterFailed] = fmt.Sprintf("%d", summaryDetails.NumberOfResources().Failed()) - row[columnCounterAll] = fmt.Sprintf("%d", summaryDetails.NumberOfResources().All()) - row[columnSeverity] = " " - row[columnComplianceScore] = fmt.Sprintf("%.2f%s", summaryDetails.ComplianceScore, "%") - - return row -} - -func (pp *PrettyPrinter) printCategoriesTables(summaryDetails *reportsummary.SummaryDetails) { - - categoriesToControlSummariesMap := mapCategoryToControlSummaries(*summaryDetails) - - categoriesTable := tablewriter.NewWriter(pp.writer) - categoriesTable.SetHeader(getCategoriesTableHeaders()) - categoriesTable.SetHeaderLine(true) - categoriesTable.SetColumnAlignment(getCategoriesColumnsAlignments()) - - for category, ctrls := range categoriesToControlSummariesMap { - renderSingleCategory(pp, category, ctrls, categoriesTable) - } - fmt.Println("") -} - -func (pp *PrettyPrinter) printSummaryTable(summaryDetails *reportsummary.SummaryDetails, sortedControlIDs [][]string) { - - if summaryDetails.NumberOfControls().All() == 0 { - fmt.Fprintf(pp.writer, "\nKubescape did not scan any of the resources, make sure you are scanning valid kubernetes manifests (Deployments, Pods, etc.)\n") - return - } - cautils.InfoTextDisplay(pp.writer, "\n"+controlCountersForSummary(summaryDetails.NumberOfControls())+"\n") - cautils.InfoTextDisplay(pp.writer, renderSeverityCountersSummary(summaryDetails.GetResourcesSeverityCounters())+"\n\n") - - // cautils.InfoTextDisplay(prettyPrinter.writer, "\n"+"Severities: SOME OTHER"+"\n\n") - - summaryTable := tablewriter.NewWriter(pp.writer) - summaryTable.SetAutoWrapText(false) - summaryTable.SetHeader(getControlTableHeaders()) - summaryTable.SetHeaderLine(true) - summaryTable.SetColumnAlignment(getColumnsAlignments()) - - printAll := pp.verboseMode - if summaryDetails.NumberOfResources().Failed() == 0 { - // if there are no failed controls, print the resource table and detailed information - printAll = true - } - - infoToPrintInfo := mapInfoToPrintInfo(summaryDetails.Controls) - for i := len(sortedControlIDs) - 1; i >= 0; i-- { - for _, c := range sortedControlIDs[i] { - row := generateRow(summaryDetails.Controls.GetControl(reportsummary.EControlCriteriaID, c), infoToPrintInfo, printAll) - if len(row) > 0 { - summaryTable.Append(row) - } - } - } - - summaryTable.SetFooter(generateFooter(summaryDetails)) - - summaryTable.Render() - - // When scanning controls the framework list will be empty - cautils.InfoTextDisplay(pp.writer, frameworksScoresToString(summaryDetails.ListFrameworks())) - - pp.printInfo(infoToPrintInfo) - -} - -func (pp *PrettyPrinter) printInfo(infoToPrintInfo []infoStars) { - fmt.Println() - for i := range infoToPrintInfo { - cautils.InfoDisplay(pp.writer, fmt.Sprintf("%s %s\n", infoToPrintInfo[i].stars, infoToPrintInfo[i].info)) - } -} func frameworksScoresToString(frameworks []reportsummary.IFrameworkSummary) string { if len(frameworks) == 1 { @@ -312,26 +221,6 @@ func frameworksScoresToString(frameworks []reportsummary.IFrameworkSummary) stri return "" } -// renderSeverityCountersSummary renders the string that reports severity counters summary -func renderSeverityCountersSummary(counters reportsummary.ISeverityCounters) string { - critical := counters.NumberOfCriticalSeverity() - high := counters.NumberOfHighSeverity() - medium := counters.NumberOfMediumSeverity() - low := counters.NumberOfLowSeverity() - - return fmt.Sprintf( - "Failed Resources by Severity: Critical — %d, High — %d, Medium — %d, Low — %d", - critical, high, medium, low, - ) -} - -func controlCountersForSummary(counters reportsummary.ICounters) string { - return fmt.Sprintf("Controls: %d (Failed: %d, Passed: %d, Action Required: %d)", counters.All(), counters.Failed(), counters.Passed(), counters.Skipped()) -} - -func controlCountersForResource(l *helpersv1.AllLists) string { - return fmt.Sprintf("Controls: %d (Failed: %d, action required: %d)", l.Len(), l.Failed(), l.Skipped()) -} func getSeparator(sep string) string { s := "" for i := 0; i < 80; i++ { diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter/clusterprinter.go b/core/pkg/resultshandling/printer/v2/prettyprinter/clusterprinter.go new file mode 100644 index 00000000..dbd0f723 --- /dev/null +++ b/core/pkg/resultshandling/printer/v2/prettyprinter/clusterprinter.go @@ -0,0 +1,61 @@ +package prettyprinter + +import ( + "fmt" + "os" + + "github.com/kubescape/kubescape/v2/core/cautils" + "github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary" + "github.com/olekukonko/tablewriter" +) + +func NewClusterPrinter() *MainPrinterImpl { + printer := &MainPrinterImpl{} + + printer.CategoriesConfig = NewCategoriesConfiguration(getCommonCategoriesTableHeaders(), getCommonColumnsAlignments()) + + printer.NextSteps = getClusterNextSteps() + + printer.TopXWorkloadsFunc = clusterTopXWorkloadsFunc + + printer.SetCompliancePrint(true) + + return printer +} + +func clusterTopXWorkloadsFunc(writer *os.File, topXWls []reportsummary.TopWorkload) { + for i, wl := range topXWls { + ns := wl.Workload.GetNamespace() + name := wl.Workload.GetName() + kind := wl.Workload.GetKind() + cautils.SimpleDisplay(writer, fmt.Sprintf("%d. namespace: %s, name: %s, kind: %s - '%s'\n", i+1, ns, name, kind, getWorkloadScanCommand(ns, kind, name))) + } + + cautils.InfoTextDisplay(writer, "\n") +} + +func getWorkloadScanCommand(namespace, kind, name string) string { + return fmt.Sprintf("$ kubescape scan workload %s/%s/%s", namespace, kind, name) +} + +func getClusterNextSteps() []string { + return []string{ + "cluster scan run: '$ kubescape scan cluster'", + "CICD setup: ", + "continues monitoring install helm: ", + } +} + +func getCommonColumnsAlignments() []int { + return []int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_LEFT} +} + +func getCommonCategoriesTableHeaders() []string { + headers := make([]string, 4) + headers[categoriesColumnSeverity] = "SEVERITY" + headers[categoriesColumnName] = "CONTROL NAME" + headers[categoriesColumnFailed] = "STATUS" + headers[categoriesColumnNextSteps] = "NEXT STEPS" + + return headers +} diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter/interface.go b/core/pkg/resultshandling/printer/v2/prettyprinter/interface.go new file mode 100644 index 00000000..93872fae --- /dev/null +++ b/core/pkg/resultshandling/printer/v2/prettyprinter/interface.go @@ -0,0 +1,11 @@ +package prettyprinter + +import ( + "os" + + "github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary" +) + +type MainPrinter interface { + Print(writer *os.File, summaryDetails *reportsummary.SummaryDetails, sortedControlIDs [][]string) +} diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter/printerfactory.go b/core/pkg/resultshandling/printer/v2/prettyprinter/printerfactory.go new file mode 100644 index 00000000..5300b9d9 --- /dev/null +++ b/core/pkg/resultshandling/printer/v2/prettyprinter/printerfactory.go @@ -0,0 +1,168 @@ +package prettyprinter + +import ( + "fmt" + "os" + + "github.com/kubescape/kubescape/v2/core/cautils" + "github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary" + "github.com/olekukonko/tablewriter" +) + +type MainPrinterImpl struct { + VerboseMode bool + SummaryPrint bool + CompliancePrint bool + NextSteps []string + + CategoriesConfig *CategoriesConfiguration + TopXWorkloadsFunc func(writer *os.File, TopXWorkloads []reportsummary.TopWorkload) +} + +var _ MainPrinter = &MainPrinterImpl{} + +func (mpi *MainPrinterImpl) GetCompliancePrint() bool { + return mpi.CompliancePrint +} + +func (mpi *MainPrinterImpl) SetVerboseMode(value bool) { + mpi.VerboseMode = value +} + +func (mpi *MainPrinterImpl) GetVerboseMode() bool { + return mpi.VerboseMode +} + +func (mpi *MainPrinterImpl) SetSummaryPrint(value bool) { + mpi.SummaryPrint = value +} + +func (mpi *MainPrinterImpl) GetSummaryPrint() bool { + return mpi.SummaryPrint +} + +func (mpi *MainPrinterImpl) SetCompliancePrint(value bool) { + mpi.CompliancePrint = value +} + +type CategoriesConfiguration struct { + Headers []string + ColumnsAlignments []int +} + +func NewCategoriesConfiguration(headers []string, columnsAlignments []int) *CategoriesConfiguration { + return &CategoriesConfiguration{ + Headers: headers, + ColumnsAlignments: columnsAlignments, + } +} + +func (cc *CategoriesConfiguration) GetCategoriesHeaders() []string { + return cc.Headers +} + +func (cc *CategoriesConfiguration) GetCategoriesColumnsAlignments() []int { + return cc.ColumnsAlignments +} + +func NewMainPrinter(scanType cautils.ScanTypes) MainPrinter { + switch scanType { + case cautils.ScanTypeCluster: + return NewClusterPrinter() + case cautils.ScanTypeRepo: + return NewRepoPrinter() + default: + return NewSummaryPrinter() + } +} + +func (mpi *MainPrinterImpl) Print(writer *os.File, summaryDetails *reportsummary.SummaryDetails, sortedControlIDs [][]string) { + if mpi.CategoriesConfig != nil { + mpi.printCategories(writer, summaryDetails, sortedControlIDs) + } + + if mpi.GetSummaryPrint() { + mpi.printSummary(writer, summaryDetails, sortedControlIDs) + } + + if mpi.TopXWorkloadsFunc != nil { + // if number of workloads is less than 3, don't use the word "most" + if len(summaryDetails.TopWorkloadsByScore) < 3 { + cautils.InfoTextDisplay(writer, "Your risky workloads:\n") + } else if len(summaryDetails.TopWorkloadsByScore) > 0 { + cautils.InfoTextDisplay(writer, "Your most risky workloads:\n") + } + + mpi.TopXWorkloadsFunc(writer, summaryDetails.TopWorkloadsByScore) + } + + if mpi.GetCompliancePrint() { + printComplianceScore(writer, filterComplianceFrameworks(summaryDetails.ListFrameworks())) + } + + if len(mpi.NextSteps) > 0 { + mpi.printNextSteps(writer) + } + + fmt.Println("") +} + +func (mpi *MainPrinterImpl) printNextSteps(writer *os.File) { + cautils.InfoTextDisplay(writer, "Follow-up Steps:\n") + for _, ns := range mpi.NextSteps { + cautils.InfoTextDisplay(writer, "- "+ns+"\n") + } +} + +func (mpi *MainPrinterImpl) printCategories(writer *os.File, summaryDetails *reportsummary.SummaryDetails, sortedControlIDs [][]string) { + categoriesToControlSummariesMap := mapCategoryToControlSummaries(*summaryDetails, sortedControlIDs) + + categoriesTable := tablewriter.NewWriter(writer) + categoriesTable.SetHeader(mpi.CategoriesConfig.GetCategoriesHeaders()) + categoriesTable.SetHeaderLine(true) + categoriesTable.SetColumnAlignment(mpi.CategoriesConfig.GetCategoriesColumnsAlignments()) + + for category, ctrls := range categoriesToControlSummariesMap { + renderSingleCategory(writer, category, ctrls, categoriesTable) + } +} + +func (mpi *MainPrinterImpl) printSummary(writer *os.File, summaryDetails *reportsummary.SummaryDetails, sortedControlIDs [][]string) { + if summaryDetails.NumberOfControls().All() == 0 { + fmt.Fprintf(writer, "\nKubescape did not scan any of the resources, make sure you are scanning valid kubernetes manifests (Deployments, Pods, etc.)\n") + return + } + cautils.InfoTextDisplay(writer, "\n"+ControlCountersForSummary(summaryDetails.NumberOfControls())+"\n") + cautils.InfoTextDisplay(writer, renderSeverityCountersSummary(summaryDetails.GetResourcesSeverityCounters())+"\n\n") + + summaryTable := tablewriter.NewWriter(writer) + summaryTable.SetAutoWrapText(false) + summaryTable.SetHeader(getControlTableHeaders()) + summaryTable.SetHeaderLine(true) + summaryTable.SetColumnAlignment(getColumnsAlignments()) + + printAll := mpi.GetVerboseMode() + if summaryDetails.NumberOfResources().Failed() == 0 { + // if there are no failed controls, print the resource table and detailed information + printAll = true + } + + infoToPrintInfo := mapInfoToPrintInfo(summaryDetails.Controls) + for i := len(sortedControlIDs) - 1; i >= 0; i-- { + for _, c := range sortedControlIDs[i] { + row := generateRow(summaryDetails.Controls.GetControl(reportsummary.EControlCriteriaID, c), infoToPrintInfo, printAll) + if len(row) > 0 { + summaryTable.Append(row) + } + } + } + + summaryTable.SetFooter(generateFooter(summaryDetails)) + + summaryTable.Render() + + // When scanning controls the framework list will be empty + cautils.InfoTextDisplay(writer, frameworksScoresToString(summaryDetails.ListFrameworks())) + + printInfo(writer, infoToPrintInfo) +} diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter/repoprinter.go b/core/pkg/resultshandling/printer/v2/prettyprinter/repoprinter.go new file mode 100644 index 00000000..a1eaa6b0 --- /dev/null +++ b/core/pkg/resultshandling/printer/v2/prettyprinter/repoprinter.go @@ -0,0 +1,55 @@ +package prettyprinter + +import ( + "fmt" + "os" + + "github.com/kubescape/kubescape/v2/core/cautils" + "github.com/kubescape/opa-utils/reporthandling" + "github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary" +) + +func NewRepoPrinter() *MainPrinterImpl { + printer := &MainPrinterImpl{} + + printer.CategoriesConfig = NewCategoriesConfiguration(getCommonCategoriesTableHeaders(), getCommonColumnsAlignments()) + + printer.NextSteps = getRepoNextSteps() + + printer.TopXWorkloadsFunc = repoTopXWorkloadsFunc + + return printer +} + +func repoTopXWorkloadsFunc(writer *os.File, topXWls []reportsummary.TopWorkload) { + for i, wl := range topXWls { + ns := wl.Workload.GetNamespace() + name := wl.Workload.GetName() + kind := wl.Workload.GetKind() + cmdPrefix := getWorkloadPrefixForCmd(ns, kind, name) + cautils.SimpleDisplay(writer, fmt.Sprintf("%d. %s - '%s'\n", i+1, cmdPrefix, getRepoWorkloadScanCommand(ns, kind, name, wl.ResourceSource))) + } + + cautils.InfoTextDisplay(writer, "\n") +} + +func getRepoWorkloadScanCommand(ns, kind, name string, source reporthandling.Source) string { + cmd := fmt.Sprintf("$ kubescape scan workload %s/%s/%s", ns, kind, name) + if ns == "" { + cmd = fmt.Sprintf("$ kubescape scan workload %s/%s", kind, name) + } + if source.FileType == "Helm" { + return fmt.Sprintf("%s --chart-path=%s", cmd, source.RelativePath) + + } else { + return fmt.Sprintf("%s --file-path=%s", cmd, source.RelativePath) + } +} + +func getRepoNextSteps() []string { + return []string{ + "cluster scan run: '$ kubescape scan cluster'", + "CICD setup: ", + "continue monitoring install helm: ", + } +} diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter/summaryprinter.go b/core/pkg/resultshandling/printer/v2/prettyprinter/summaryprinter.go new file mode 100644 index 00000000..21964733 --- /dev/null +++ b/core/pkg/resultshandling/printer/v2/prettyprinter/summaryprinter.go @@ -0,0 +1,17 @@ +package prettyprinter + +const ( + summaryColumnSeverity = iota + summaryColumnName = iota + summaryColumnCounterFailed = iota + summaryColumnCounterAll = iota + summaryColumnComplianceScore = iota + _summaryRowLen = iota +) + +func NewSummaryPrinter() *MainPrinterImpl { + printer := &MainPrinterImpl{} + printer.SetSummaryPrint(true) + + return printer +} diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter/utils.go b/core/pkg/resultshandling/printer/v2/prettyprinter/utils.go new file mode 100644 index 00000000..023ba108 --- /dev/null +++ b/core/pkg/resultshandling/printer/v2/prettyprinter/utils.go @@ -0,0 +1,291 @@ +package prettyprinter + +import ( + "fmt" + "os" + "strings" + + "github.com/fatih/color" + "github.com/kubescape/kubescape/v2/core/cautils" + "github.com/kubescape/opa-utils/reporthandling/apis" + helpersv1 "github.com/kubescape/opa-utils/reporthandling/helpers/v1" + "github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary" + "github.com/olekukonko/tablewriter" + "k8s.io/utils/strings/slices" +) + +const ( + categoriesColumnSeverity = iota + categoriesColumnName = iota + categoriesColumnFailed = iota + categoriesColumnNextSteps = iota +) + +var ( + mapScanTypeToOutput = map[cautils.ScanTypes]string{ + cautils.ScanTypeCluster: "Security Overview", + } + complianceFrameworks = []string{"nsa", "mitre"} +) + +func mapCategoryToControlSummaries(summaryDetails reportsummary.SummaryDetails, sortedControlIDs [][]string) map[string][]reportsummary.IControlSummary { + categories := map[string][]reportsummary.IControlSummary{} + + for i := len(sortedControlIDs) - 1; i >= 0; i-- { + for _, c := range sortedControlIDs[i] { + ctrl := summaryDetails.Controls.GetControl(reportsummary.EControlCriteriaID, c) + if ctrl.GetStatus().Status() == apis.StatusPassed { + continue + } + for j := range ctrl.GetCategories() { + categories[ctrl.GetCategories()[j]] = append(categories[ctrl.GetCategories()[j]], ctrl) + } + } + } + + return categories +} + +func generateCategoriesRow(controlSummary reportsummary.IControlSummary) []string { + row := make([]string, 4) + row[categoriesColumnSeverity] = getSeverityColumn(controlSummary) + + if len(controlSummary.GetName()) > 50 { + row[categoriesColumnName] = controlSummary.GetName()[:50] + "..." + } else { + row[categoriesColumnName] = controlSummary.GetName() + } + + row[categoriesColumnFailed] = fmt.Sprintf("%s", controlSummary.GetStatus().Status()) + row[categoriesColumnNextSteps] = generateNextSteps(controlSummary) + + return row +} + +func generateNextSteps(controlSummary reportsummary.IControlSummary) string { + return fmt.Sprintf("$ kubescape scan control %s", controlSummary.GetID()) +} + +func getCategoriesTableHeaders() []string { + headers := make([]string, 4) + headers[categoriesColumnSeverity] = "SEVERITY" + headers[categoriesColumnName] = "CONTROL NAME" + headers[categoriesColumnFailed] = "FAILED RESOURCES" + headers[categoriesColumnNextSteps] = "NEXT STEPS" + + return headers +} + +func getCategoriesColumnsAlignments() []int { + alignments := make([]int, 4) + alignments[categoriesColumnSeverity] = tablewriter.ALIGN_LEFT + alignments[categoriesColumnName] = tablewriter.ALIGN_LEFT + alignments[categoriesColumnFailed] = tablewriter.ALIGN_CENTER + alignments[categoriesColumnNextSteps] = tablewriter.ALIGN_LEFT + + return alignments +} + +func renderSingleCategory(writer *os.File, category string, ctrls []reportsummary.IControlSummary, categoriesTable *tablewriter.Table) { + cautils.InfoTextDisplay(writer, "\n"+category+"\n") + + categoriesTable.ClearRows() + for i := range ctrls { + row := generateCategoriesRow(ctrls[i]) + if len(row) > 0 { + categoriesTable.Append(row) + } + } + categoriesTable.Render() +} + +func getSeverityColumn(controlSummary reportsummary.IControlSummary) string { + return color.New(getColor(apis.ControlSeverityToInt(controlSummary.GetScoreFactor())), color.Bold).SprintFunc()(apis.ControlSeverityToString(controlSummary.GetScoreFactor())) +} + +func getColor(controlSeverity int) color.Attribute { + switch controlSeverity { + case apis.SeverityCritical: + return color.FgRed + case apis.SeverityHigh: + return color.FgYellow + case apis.SeverityMedium: + return color.FgCyan + case apis.SeverityLow: + return color.FgWhite + default: + return color.FgWhite + } +} + +type infoStars struct { + stars string + info string +} + +func mapInfoToPrintInfo(controls reportsummary.ControlSummaries) []infoStars { + infoToPrintInfo := []infoStars{} + infoToPrintInfoMap := map[string]interface{}{} + starCount := "*" + for _, control := range controls { + if control.GetStatus().IsSkipped() && control.GetStatus().Info() != "" { + if _, ok := infoToPrintInfoMap[control.GetStatus().Info()]; !ok { + infoToPrintInfo = append(infoToPrintInfo, infoStars{ + info: control.GetStatus().Info(), + stars: starCount, + }) + starCount += "*" + infoToPrintInfoMap[control.GetStatus().Info()] = nil + } + } + } + return infoToPrintInfo +} + +func printInfo(writer *os.File, infoToPrintInfo []infoStars) { + fmt.Println() + for i := range infoToPrintInfo { + cautils.InfoDisplay(writer, fmt.Sprintf("%s %s\n", infoToPrintInfo[i].stars, infoToPrintInfo[i].info)) + } +} +func frameworksScoresToString(frameworks []reportsummary.IFrameworkSummary) string { + if len(frameworks) == 1 { + if frameworks[0].GetName() != "" { + return fmt.Sprintf("FRAMEWORK %s\n", frameworks[0].GetName()) + // cautils.InfoTextDisplay(prettyPrinter.writer, )) + } + } else if len(frameworks) > 1 { + p := "FRAMEWORKS: " + i := 0 + for ; i < len(frameworks)-1; i++ { + p += fmt.Sprintf("%s (compliance: %.2f), ", frameworks[i].GetName(), frameworks[i].GetComplianceScore()) + } + p += fmt.Sprintf("%s (compliance: %.2f)\n", frameworks[i].GetName(), frameworks[i].GetComplianceScore()) + return p + } + return "" +} + +func printComplianceScore(writer *os.File, frameworks []reportsummary.IFrameworkSummary) { + cautils.InfoTextDisplay(writer, "Compliance Score:\n") + for _, fw := range frameworks { + cautils.SimpleDisplay(writer, "* %s: %.2f\n", fw.GetName(), fw.GetComplianceScore()) + } + + cautils.InfoTextDisplay(writer, "\n") +} + +func filterComplianceFrameworks(frameworks []reportsummary.IFrameworkSummary) []reportsummary.IFrameworkSummary { + complianceFws := []reportsummary.IFrameworkSummary{} + for _, fw := range frameworks { + if slices.Contains(complianceFrameworks, strings.ToLower(fw.GetName())) { + complianceFws = append(complianceFws, fw) + } + } + return complianceFws +} + +func getSeparator(sep string) string { + s := "" + for i := 0; i < 80; i++ { + s += sep + } + return s +} + +func generateFooter(summaryDetails *reportsummary.SummaryDetails) []string { + // Severity | Control name | failed resources | all resources | % success + row := make([]string, _summaryRowLen) + row[summaryColumnName] = "Resource Summary" + row[summaryColumnCounterFailed] = fmt.Sprintf("%d", summaryDetails.NumberOfResources().Failed()) + row[summaryColumnCounterAll] = fmt.Sprintf("%d", summaryDetails.NumberOfResources().All()) + row[summaryColumnSeverity] = " " + row[summaryColumnComplianceScore] = fmt.Sprintf("%.2f%s", summaryDetails.ComplianceScore, "%") + + return row +} + +func ControlCountersForSummary(counters reportsummary.ICounters) string { + return fmt.Sprintf("Controls: %d (Failed: %d, Passed: %d, Action Required: %d)", counters.All(), counters.Failed(), counters.Passed(), counters.Skipped()) +} + +func ControlCountersForResource(l *helpersv1.AllLists) string { + return fmt.Sprintf("Controls: %d (Failed: %d, action required: %d)", l.Len(), l.Failed(), l.Skipped()) +} + +// renderSeverityCountersSummary renders the string that reports severity counters summary +func renderSeverityCountersSummary(counters reportsummary.ISeverityCounters) string { + critical := counters.NumberOfCriticalSeverity() + high := counters.NumberOfHighSeverity() + medium := counters.NumberOfMediumSeverity() + low := counters.NumberOfLowSeverity() + + return fmt.Sprintf( + "Failed Resources by Severity: Critical — %d, High — %d, Medium — %d, Low — %d", + critical, high, medium, low, + ) +} + +func getControlTableHeaders() []string { + headers := make([]string, _summaryRowLen) + headers[summaryColumnName] = "CONTROL NAME" + headers[summaryColumnCounterFailed] = "FAILED RESOURCES" + headers[summaryColumnCounterAll] = "ALL RESOURCES" + headers[summaryColumnSeverity] = "SEVERITY" + headers[summaryColumnComplianceScore] = "% COMPLIANCE-SCORE" + return headers +} + +func getColumnsAlignments() []int { + alignments := make([]int, _summaryRowLen) + alignments[summaryColumnName] = tablewriter.ALIGN_LEFT + alignments[summaryColumnCounterFailed] = tablewriter.ALIGN_CENTER + alignments[summaryColumnCounterAll] = tablewriter.ALIGN_CENTER + alignments[summaryColumnSeverity] = tablewriter.ALIGN_LEFT + alignments[summaryColumnComplianceScore] = tablewriter.ALIGN_CENTER + return alignments +} + +func generateRow(controlSummary reportsummary.IControlSummary, infoToPrintInfo []infoStars, verbose bool) []string { + row := make([]string, _summaryRowLen) + + // ignore passed results + if !verbose && (controlSummary.GetStatus().IsPassed()) { + return []string{} + } + + row[summaryColumnSeverity] = getSeverityColumn(controlSummary) + if len(controlSummary.GetName()) > 50 { + row[summaryColumnName] = controlSummary.GetName()[:50] + "..." + } else { + row[summaryColumnName] = controlSummary.GetName() + } + row[summaryColumnCounterFailed] = fmt.Sprintf("%d", controlSummary.NumberOfResources().Failed()) + row[summaryColumnCounterAll] = fmt.Sprintf("%d", controlSummary.NumberOfResources().All()) + row[summaryColumnComplianceScore] = getComplianceScoreColumn(controlSummary, infoToPrintInfo) + + return row +} + +func getComplianceScoreColumn(controlSummary reportsummary.IControlSummary, infoToPrintInfo []infoStars) string { + if controlSummary.GetStatus().IsSkipped() { + return fmt.Sprintf("%s %s", "Action Required", getInfoColumn(controlSummary, infoToPrintInfo)) + } + return fmt.Sprintf("%d", cautils.Float32ToInt(controlSummary.GetComplianceScore())) + "%" +} + +func getInfoColumn(controlSummary reportsummary.IControlSummary, infoToPrintInfo []infoStars) string { + for i := range infoToPrintInfo { + if infoToPrintInfo[i].info == controlSummary.GetStatus().Info() { + return infoToPrintInfo[i].stars + } + } + return "" +} + +func getWorkloadPrefixForCmd(namespace, kind, name string) string { + if namespace == "" { + return fmt.Sprintf("name: %s, kind: %s", name, kind) + } + return fmt.Sprintf("namespace: %s, name: %s, kind: %s", namespace, name, kind) +} diff --git a/core/pkg/resultshandling/printer/v2/resourcetable.go b/core/pkg/resultshandling/printer/v2/resourcetable.go index 5d1fe8c9..da0037d9 100644 --- a/core/pkg/resultshandling/printer/v2/resourcetable.go +++ b/core/pkg/resultshandling/printer/v2/resourcetable.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/kubescape/kubescape/v2/core/cautils" + "github.com/kubescape/kubescape/v2/core/pkg/resultshandling/printer/v2/prettyprinter" "github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary" "github.com/kubescape/opa-utils/reporthandling/results/v1/resourcesresults" "github.com/olekukonko/tablewriter" @@ -40,7 +41,7 @@ func (prettyPrinter *PrettyPrinter) resourceTable(opaSessionObj *cautils.OPASess if resource.GetNamespace() != "" { fmt.Fprintf(prettyPrinter.writer, "Namespace: %s\n", resource.GetNamespace()) } - fmt.Fprintf(prettyPrinter.writer, "\n"+controlCountersForResource(result.ListControlsIDs(nil))+"\n\n") + fmt.Fprintf(prettyPrinter.writer, "\n"+prettyprinter.ControlCountersForResource(result.ListControlsIDs(nil))+"\n\n") summaryTable := tablewriter.NewWriter(prettyPrinter.writer) summaryTable.SetAutoWrapText(true) diff --git a/core/pkg/resultshandling/printer/v2/utils.go b/core/pkg/resultshandling/printer/v2/utils.go index ca2fa192..eed78776 100644 --- a/core/pkg/resultshandling/printer/v2/utils.go +++ b/core/pkg/resultshandling/printer/v2/utils.go @@ -82,13 +82,3 @@ func finalizeResources(results []resourcesresults.Result, allResources map[strin } return resources } - -func mapCategoryToControlSummaries(summaryDetails reportsummary.SummaryDetails) map[string][]reportsummary.IControlSummary { - categories := map[string][]reportsummary.IControlSummary{} - for i := range summaryDetails.Controls { - for j := range summaryDetails.Controls[i].Categories { - categories[summaryDetails.Controls[i].Categories[j]] = append(categories[summaryDetails.Controls[i].Categories[j]], summaryDetails.Controls.GetControl(reportsummary.EControlCriteriaID, summaryDetails.Controls[i].ControlID)) - } - } - return categories -}