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
-}