Signed-off-by: Daniel Grunberger <danielgrunberger@armosec.io>
This commit is contained in:
Daniel Grunberger
2023-07-10 09:10:37 +03:00
parent fac2f7bec1
commit f54bfabacc
16 changed files with 643 additions and 243 deletions
+1
View File
@@ -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 !!
+20 -1
View File
@@ -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{}
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -286,7 +286,7 @@ func getAttackTracksGetter(ctx context.Context, attackTracks, accountID string,
}
// getUIPrinter returns a printer that will be used to print to the programs 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{}
+10 -3
View File
@@ -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
}
@@ -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()
}
@@ -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("")
}
@@ -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 arent 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++ {
@@ -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: <link to docs>",
"continues monitoring install helm: <docs to 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
}
@@ -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)
}
@@ -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)
}
@@ -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: <link to docs>",
"continue monitoring install helm: <docs to helm>",
}
}
@@ -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
}
@@ -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)
}
@@ -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)
@@ -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
}