diff --git a/cmd/patch/README.md b/cmd/image/README.md similarity index 54% rename from cmd/patch/README.md rename to cmd/image/README.md index cb4ba6ec..b2866a9f 100644 --- a/cmd/patch/README.md +++ b/cmd/image/README.md @@ -1,3 +1,90 @@ +# Scan Command + +The scan command is used for scanning container images for vulnerabilities. +It uses [grype](https://github.com/anchore/grype) under the hood for scanning the images. + +## Usage + + +```bash +kubescape image scan [flags] +``` + +### Flags +| Flag | Description | Required | Default | +| ------------------------- | ------------------------------------------------------ |--------- | --------| +| -u, --username | Username for the image registry login | No | | +| -p, --password | Password for the image registry login | No | | +| -f, --format | Output file format. | No | | +| -o, --output | Output file. Print output to file and not stdout | No | | +| -v, --verbose | Display full report | No | false | +| -s, --severity-threshold | Maximum severity of a vulnerability | No | | +| -h, --help | help for scan | No | | + + +## Example + +1. In a seperate terminal, run the `kubescape scan` command: + + ```bash + sudo kubescape image scan nginx:1.22 + ``` + +2. You will get an output like below: + + ```bash + ✅ Successfully scanned image: nginx:1.22 + | Severity | Vulnerability | Component | Version | Fixed In | + | -------- | -------------- | ------------- | ----------------------- | -------- | + | Critical | CVE-2023-23914 | curl | 7.74.0-1.3+deb11u7 | wont-fix | + | Critical | CVE-2019-8457 | libdb5.3 | 5.3.28+dfsg1-0.8 | wont-fix | + | High | CVE-2022-42916 | libcurl4 | 7.74.0-1.3+deb11u7 | wont-fix | + | High | CVE-2022-1304 | libext2fs2 | 1.46.2-2 | wont-fix | + | High | CVE-2022-42916 | curl | 7.74.0-1.3+deb11u7 | wont-fix | + | High | CVE-2022-1304 | e2fsprogs | 1.46.2-2 | wont-fix | + | High | CVE-2022-1304 | libcom-err2 | 1.46.2-2 | wont-fix | + | High | CVE-2023-27533 | curl | 7.74.0-1.3+deb11u7 | wont-fix | + | High | CVE-2023-27534 | libcurl4 | 7.74.0-1.3+deb11u7 | wont-fix | + | High | CVE-2023-27533 | libcurl4 | 7.74.0-1.3+deb11u7 | wont-fix | + | High | CVE-2022-43551 | libcurl4 | 7.74.0-1.3+deb11u7 | wont-fix | + | High | CVE-2022-3715 | bash | 5.1-2+deb11u1 | wont-fix | + | High | CVE-2023-27534 | curl | 7.74.0-1.3+deb11u7 | wont-fix | + | High | CVE-2022-43551 | curl | 7.74.0-1.3+deb11u7 | wont-fix | + | High | CVE-2021-33560 | libgcrypt20 | 1.8.7-6 | wont-fix | + | High | CVE-2023-2953 | libldap-2.4-2 | 2.4.57+dfsg-3+deb11u1 | wont-fix | + | High | CVE-2022-1304 | libss2 | 1.46.2-2 | wont-fix | + | High | CVE-2020-22218 | libssh2-1 | 1.9.0-2 | wont-fix | + | High | CVE-2023-29491 | libtinfo6 | 6.2+20201114-2+deb11u1 | wont-fix | + | High | CVE-2022-2309 | libxml2 | 2.9.10+dfsg-6.7+deb11u4 | wont-fix | + | High | CVE-2022-4899 | libzstd1 | 1.4.8+dfsg-2.1 | wont-fix | + | High | CVE-2022-1304 | logsave | 1.46.2-2 | wont-fix | + | High | CVE-2023-29491 | ncurses-base | 6.2+20201114-2+deb11u1 | wont-fix | + | High | CVE-2023-29491 | ncurses-bin | 6.2+20201114-2+deb11u1 | wont-fix | + | High | CVE-2023-31484 | perl-base | 5.32.1-4+deb11u2 | wont-fix | + | High | CVE-2020-16156 | perl-base | 5.32.1-4+deb11u2 | wont-fix | + + Vulnerability summary - 184 vulnerabilities found: + ────────────────────────────────────────────────── + Image: nginx:1.22 + * 3 Critical + * 35 High + * 43 Medium + * 103 Other + + Most vulnerable components: + * curl (7.74.0-1.3+deb11u7) - 1 Critical, 4 High, 5 Medium, 1 Low, 3 Negligible + * libcurl4 (7.74.0-1.3+deb11u7) - 1 Critical, 4 High, 5 Medium, 1 Low, 3 Negligible + * libtiff5 (4.2.0-1+deb11u4) - 7 Medium, 10 Negligible, 2 Unknown + * libssl1.1 (1.1.1n-0+deb11u4) - 1 High, 5 Medium, 2 Negligible + * openssl (1.1.1n-0+deb11u4) - 1 High, 5 Medium, 2 Negligible + + What now? + ───────── + * Run with '--verbose'/'-v' flag for detailed vulnerabilities view + * Install Kubescape in your cluster for continuous monitoring and a full vulnerability report: https://github.com/kubescape/helm-charts/tree/main/charts/kubescape-cloud-operator + ``` + + # Patch Command The patch command is used for patching container images with vulnerabilities. @@ -6,7 +93,7 @@ It uses [copa](https://github.com/project-copacetic/copacetic) and [buildkit](ht ## Usage ```bash -kubescape patch --image [flags] +kubescape image patch [flags] ``` The patch command can be run in 2 ways: @@ -43,12 +130,14 @@ The patch command can be run in 2 ways: | Flag | Description | Required | Default | | -------------- | ------------------------------------------------------ | -------- | ----------------------------------- | -| -i, --image | Image name to be patched (should be in canonical form) | Yes | | | -a, --addr | Address of the buildkitd service | No | unix:///run/buildkit/buildkitd.sock | | -t, --tag | Tag of the resultant patched image | No | image_name-patched | | --timeout | Timeout for the patching process | No | 5m | | -u, --username | Username for the image registry login | No | | | -p, --password | Password for the image registry login | No | | +| -f, --format | Output file format. | No | | +| -o, --output | Output file. Print output to file and not stdout | No | | +| -v, --verbose | Display full report. Default to false | No | | | -h, --help | help for patch | No | | @@ -72,7 +161,7 @@ We will demonstrate how to use the patch command with an example of [nginx](http 2. In a seperate terminal, run the `kubescape patch` command: ```bash - sudo kubescape patch --image docker.io/library/nginx:1.22 + sudo kubescape image patch docker.io/library/nginx:1.22 ``` 3. You will get an output like below: diff --git a/cmd/image/image.go b/cmd/image/image.go new file mode 100644 index 00000000..501adcba --- /dev/null +++ b/cmd/image/image.go @@ -0,0 +1,58 @@ +package image + +import ( + "fmt" + + "github.com/kubescape/kubescape/v2/core/cautils" + "github.com/kubescape/kubescape/v2/core/meta" + metav1 "github.com/kubescape/kubescape/v2/core/meta/datastructures/v1" + "github.com/spf13/cobra" +) + +var imageCmdExamples = fmt.Sprintf(` +# Scan an image +%[1]s image scan : + +# Patch an image +%[1]s image patch : +`, cautils.ExecName()) + +func GetImageCmd(ks meta.IKubescape) *cobra.Command { + var scanInfo cautils.ScanInfo + + imageCmd := &cobra.Command{ + Use: "image", + Short: "Scan or patch an image", + Example: imageCmdExamples, + } + + imgScanInfo := &metav1.ImageScanInfo{} + + imageCmd.PersistentFlags().StringVarP(&scanInfo.Format, "format", "f", "", `Output file format. Supported formats: "pretty-printer", "json", "sarif"`) + imageCmd.PersistentFlags().StringVarP(&scanInfo.Output, "output", "o", "", "Output file. Print output to file and not stdout") + imageCmd.PersistentFlags().BoolVarP(&scanInfo.VerboseMode, "verbose", "v", false, "Display full report. Default to false") + + imageCmd.PersistentFlags().StringVarP(&scanInfo.FailThresholdSeverity, "severity-threshold", "s", "", "Severity threshold is the severity of a vulnerability at which the command fails and returns exit code 1") + + imageCmd.PersistentFlags().StringVarP(&imgScanInfo.Username, "username", "u", "", "Username for registry login") + imageCmd.PersistentFlags().StringVarP(&imgScanInfo.Password, "password", "p", "", "Password for registry login") + + imageCmd.AddCommand(getScanCmd(ks, &scanInfo, imgScanInfo)) + imageCmd.AddCommand(getPatchCmd(ks, &scanInfo, imgScanInfo)) + + imageCmd.SetHelpFunc(func(command *cobra.Command, strings []string) { + // hide kube-context and server flags + command.Flags().MarkHidden("kube-context") + command.Flags().MarkHidden("server") + + // this will prevent an infinite recursive call for the sub-commands of 'image' + if parent := command.Parent().Parent(); parent != nil { + parent.HelpFunc()(command, strings) + return + } + + command.Parent().HelpFunc()(command, strings) + }) + + return imageCmd +} diff --git a/cmd/patch/patch.go b/cmd/image/patch.go similarity index 84% rename from cmd/patch/patch.go rename to cmd/image/patch.go index 28e1e31f..4615fc85 100644 --- a/cmd/patch/patch.go +++ b/cmd/image/patch.go @@ -1,4 +1,4 @@ -package patch +package image import ( "context" @@ -21,22 +21,37 @@ import ( var patchCmdExamples = fmt.Sprintf(` # Patch the nginx:1.22 image 1) sudo buildkitd # start buildkitd service, run in seperate terminal - 2) sudo %[1]s patch --image docker.io/library/nginx:1.22 # patch the image + 2) sudo %[1]s patch nginx:1.22 # patch the image # The patch command can also be run without sudo privileges # Documentation: https://github.com/kubescape/kubescape/tree/master/cmd/patch `, cautils.ExecName()) -func GetPatchCmd(ks meta.IKubescape) *cobra.Command { - var patchInfo metav1.PatchInfo +func getPatchCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo, imgScanInfo *metav1.ImageScanInfo) *cobra.Command { + patchInfo := metav1.PatchInfo{ + Username: imgScanInfo.Username, + Password: imgScanInfo.Password, + } patchCmd := &cobra.Command{ - Use: "patch --image [flags]", + Use: "patch : [flags]", Short: "Patch container images with vulnerabilities ", Long: `Patch command is for automatically patching images with vulnerabilities.`, Example: patchCmdExamples, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return fmt.Errorf("the command takes exactly one image name as an argument") + } + return nil + }, RunE: func(cmd *cobra.Command, args []string) error { + if err := validateImageScanInfo(scanInfo); err != nil { + return err + } + + patchInfo.Image = args[0] + if err := validateImagePatchInfo(&patchInfo); err != nil { return err } @@ -45,14 +60,10 @@ func GetPatchCmd(ks meta.IKubescape) *cobra.Command { }, } - patchCmd.PersistentFlags().StringVarP(&patchInfo.Image, "image", "i", "", "Application image name and tag to patch") patchCmd.PersistentFlags().StringVarP(&patchInfo.PatchedImageTag, "tag", "t", "", "Tag for the patched image. Defaults to '-patched' ") patchCmd.PersistentFlags().StringVarP(&patchInfo.BuildkitAddress, "address", "a", "unix:///run/buildkit/buildkitd.sock", "Address of buildkitd service, defaults to local buildkitd.sock") patchCmd.PersistentFlags().DurationVar(&patchInfo.Timeout, "timeout", 5*time.Minute, "Timeout for the operation, defaults to '5m'") - patchCmd.PersistentFlags().StringVarP(&patchInfo.Username, "username", "u", "", "Username for registry login") - patchCmd.PersistentFlags().StringVarP(&patchInfo.Password, "password", "p", "", "Password for registry login") - return patchCmd } diff --git a/cmd/image/scan.go b/cmd/image/scan.go new file mode 100644 index 00000000..36daa555 --- /dev/null +++ b/cmd/image/scan.go @@ -0,0 +1,73 @@ +package image + +import ( + "context" + "fmt" + + logger "github.com/kubescape/go-logger" + "github.com/kubescape/kubescape/v2/cmd/utils" + "github.com/kubescape/kubescape/v2/core/cautils" + "github.com/kubescape/kubescape/v2/core/meta" + metav1 "github.com/kubescape/kubescape/v2/core/meta/datastructures/v1" + "github.com/kubescape/kubescape/v2/pkg/imagescan" + "github.com/spf13/cobra" +) + +// TODO(vladklokun): document image scanning on the Kubescape Docs Hub? +var ( + imageExample = fmt.Sprintf(` + Scan an image for vulnerabilities. + + # Scan the 'nginx' image + %[1]s scan image "nginx" +`, cautils.ExecName()) +) + +// imageCmd represents the image command +func getScanCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo, imgScanInfo *metav1.ImageScanInfo) *cobra.Command { + cmd := &cobra.Command{ + Use: "scan : [flags]", + Short: "Scan container images for vulnerabilities", + Example: imageExample, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return fmt.Errorf("the command takes exactly one image name as an argument") + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + if err := validateImageScanInfo(scanInfo); err != nil { + return err + } + + imgScanInfo := &metav1.ImageScanInfo{ + Image: args[0], + Username: imgScanInfo.Username, + Password: imgScanInfo.Password, + } + + results, err := ks.ScanImage(context.Background(), imgScanInfo, scanInfo) + if err != nil { + return err + } + + if imagescan.ExceedsSeverityThreshold(results, imagescan.ParseSeverity(scanInfo.FailThresholdSeverity)) { + utils.TerminateOnExceedingSeverity(scanInfo, logger.L()) + } + + return err + }, + } + + return cmd +} + +// validateImageScanInfo validates the ScanInfo struct for the `image` command +func validateImageScanInfo(scanInfo *cautils.ScanInfo) error { + severity := scanInfo.FailThresholdSeverity + + if err := utils.ValidateSeverity(severity); severity != "" && err != nil { + return err + } + return nil +} diff --git a/cmd/root.go b/cmd/root.go index 99100fba..db77cf5a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -11,8 +11,8 @@ import ( "github.com/kubescape/kubescape/v2/cmd/config" "github.com/kubescape/kubescape/v2/cmd/download" "github.com/kubescape/kubescape/v2/cmd/fix" + "github.com/kubescape/kubescape/v2/cmd/image" "github.com/kubescape/kubescape/v2/cmd/list" - "github.com/kubescape/kubescape/v2/cmd/patch" "github.com/kubescape/kubescape/v2/cmd/scan" "github.com/kubescape/kubescape/v2/cmd/update" "github.com/kubescape/kubescape/v2/cmd/version" @@ -95,7 +95,7 @@ func getRootCmd(ks meta.IKubescape) *cobra.Command { rootCmd.AddCommand(config.GetConfigCmd(ks)) rootCmd.AddCommand(update.GetUpdateCmd()) rootCmd.AddCommand(fix.GetFixCmd(ks)) - rootCmd.AddCommand(patch.GetPatchCmd(ks)) + rootCmd.AddCommand(image.GetImageCmd(ks)) // deprecated commands rootCmd.AddCommand(&cobra.Command{ diff --git a/cmd/scan/control.go b/cmd/scan/control.go index dacf7bfe..f4e03531 100644 --- a/cmd/scan/control.go +++ b/cmd/scan/control.go @@ -11,6 +11,7 @@ import ( logger "github.com/kubescape/go-logger" "github.com/kubescape/go-logger/helpers" + "github.com/kubescape/kubescape/v2/cmd/utils" "github.com/kubescape/kubescape/v2/core/cautils" "github.com/kubescape/kubescape/v2/core/meta" @@ -114,7 +115,7 @@ func getControlCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Comman if results.GetComplianceScore() < float32(scanInfo.ComplianceThreshold) { logger.L().Fatal("scan compliance-score is below permitted threshold", helpers.String("compliance score", fmt.Sprintf("%.2f", results.GetComplianceScore())), helpers.String("compliance-threshold", fmt.Sprintf("%.2f", scanInfo.ComplianceThreshold))) } - enforceSeverityThresholds(results.GetResults().SummaryDetails.GetResourcesSeverityCounters(), scanInfo, terminateOnExceedingSeverity) + enforceSeverityThresholds(results.GetResults().SummaryDetails.GetResourcesSeverityCounters(), scanInfo, utils.TerminateOnExceedingSeverity) return nil }, @@ -129,7 +130,7 @@ func validateControlScanInfo(scanInfo *cautils.ScanInfo) error { return fmt.Errorf("you can use `omit-raw-resources` or `submit`, but not both") } - if err := validateSeverity(severity); severity != "" && err != nil { + if err := utils.ValidateSeverity(severity); severity != "" && err != nil { return err } return nil diff --git a/cmd/scan/framework.go b/cmd/scan/framework.go index b6704730..8d1c2327 100644 --- a/cmd/scan/framework.go +++ b/cmd/scan/framework.go @@ -15,6 +15,7 @@ import ( logger "github.com/kubescape/go-logger" "github.com/kubescape/go-logger/helpers" + "github.com/kubescape/kubescape/v2/cmd/utils" "github.com/kubescape/kubescape/v2/core/cautils" "github.com/kubescape/kubescape/v2/core/cautils/getter" "github.com/kubescape/kubescape/v2/core/meta" @@ -136,7 +137,7 @@ func getFrameworkCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Comm logger.L().Fatal("scan compliance-score is below permitted threshold", helpers.String("compliance-score", fmt.Sprintf("%.2f", results.GetComplianceScore())), helpers.String("compliance-threshold", fmt.Sprintf("%.2f", scanInfo.ComplianceThreshold))) } - enforceSeverityThresholds(results.GetData().Report.SummaryDetails.GetResourcesSeverityCounters(), scanInfo, terminateOnExceedingSeverity) + enforceSeverityThresholds(results.GetData().Report.SummaryDetails.GetResourcesSeverityCounters(), scanInfo, utils.TerminateOnExceedingSeverity) return nil }, } @@ -145,7 +146,7 @@ func getFrameworkCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo) *cobra.Comm // countersExceedSeverityThreshold returns true if severity of failed controls exceed the set severity threshold, else returns false func countersExceedSeverityThreshold(severityCounters reportsummary.ISeverityCounters, scanInfo *cautils.ScanInfo) (bool, error) { targetSeverity := scanInfo.FailThresholdSeverity - if err := validateSeverity(targetSeverity); err != nil { + if err := utils.ValidateSeverity(targetSeverity); err != nil { return false, err } @@ -178,11 +179,6 @@ func countersExceedSeverityThreshold(severityCounters reportsummary.ISeverityCou } -// terminateOnExceedingSeverity terminates the application on exceeding severity -func terminateOnExceedingSeverity(scanInfo *cautils.ScanInfo, l helpers.ILogger) { - l.Fatal("result exceeds severity threshold", helpers.String("set severity threshold", scanInfo.FailThresholdSeverity)) -} - // enforceSeverityThresholds ensures that the scan results are below the defined severity threshold // // The function forces the application to terminate with an exit code 1 if at least one control failed control that exceeds the set severity threshold @@ -199,17 +195,6 @@ func enforceSeverityThresholds(severityCounters reportsummary.ISeverityCounters, } } -// validateSeverity returns an error if a given severity is not known, nil otherwise -func validateSeverity(severity string) error { - for _, val := range reporthandlingapis.GetSupportedSeverities() { - if strings.EqualFold(severity, val) { - return nil - } - } - return ErrUnknownSeverity - -} - // validateFrameworkScanInfo validates the scan info struct for the `scan framework` command func validateFrameworkScanInfo(scanInfo *cautils.ScanInfo) error { if scanInfo.View == string(cautils.SecurityViewType) { @@ -229,7 +214,7 @@ func validateFrameworkScanInfo(scanInfo *cautils.ScanInfo) error { return ErrOmitRawResourcesOrSubmit } severity := scanInfo.FailThresholdSeverity - if err := validateSeverity(severity); severity != "" && err != nil { + if err := utils.ValidateSeverity(severity); severity != "" && err != nil { return err } diff --git a/cmd/scan/image.go b/cmd/scan/image.go deleted file mode 100644 index 6d84bac0..00000000 --- a/cmd/scan/image.go +++ /dev/null @@ -1,114 +0,0 @@ -package scan - -import ( - "context" - "fmt" - - logger "github.com/kubescape/go-logger" - "github.com/kubescape/kubescape/v2/core/cautils" - "github.com/kubescape/kubescape/v2/core/core" - "github.com/kubescape/kubescape/v2/core/meta" - "github.com/kubescape/kubescape/v2/core/pkg/resultshandling" - "github.com/kubescape/kubescape/v2/pkg/imagescan" - - "github.com/spf13/cobra" -) - -type imageScanInfo struct { - Username string - Password string -} - -// TODO(vladklokun): document image scanning on the Kubescape Docs Hub? -var ( - imageExample = fmt.Sprintf(` - This command is still in BETA. Feel free to contact the Kubescape maintainers for more information. - - Scan an image for vulnerabilities. - - # Scan the 'nginx' image - %[1]s scan image "nginx" - - # Image scan documentation: - # https://hub.armosec.io/docs/images -`, cautils.ExecName()) -) - -// imageCmd represents the image command -func getImageCmd(ks meta.IKubescape, scanInfo *cautils.ScanInfo, imgScanInfo *imageScanInfo) *cobra.Command { - cmd := &cobra.Command{ - Use: "image ", - Short: "Scan an image for vulnerabilities", - Example: imageExample, - Args: func(cmd *cobra.Command, args []string) error { - if len(args) != 1 { - return fmt.Errorf("the command takes exactly one image name as an argument") - } - return nil - }, - RunE: func(cmd *cobra.Command, args []string) error { - if err := validateImageScanInfo(scanInfo); err != nil { - return err - } - failOnSeverity := imagescan.ParseSeverity(scanInfo.FailThresholdSeverity) - - ctx := context.Background() - - dbCfg, _ := imagescan.NewDefaultDBConfig() - svc := imagescan.NewScanService(dbCfg) - - creds := imagescan.RegistryCredentials{ - Username: imgScanInfo.Username, - Password: imgScanInfo.Password, - } - - userInput := args[0] - - logger.L().Start(fmt.Sprintf("Scanning image: %s", userInput)) - scanResults, err := svc.Scan(ctx, userInput, creds) - if err != nil { - logger.L().StopError(fmt.Sprintf("Failed to scan image: %s", userInput)) - return err - } - logger.L().StopSuccess(fmt.Sprintf("Successfully scanned image: %s", userInput)) - - scanInfo.SetScanType(cautils.ScanTypeImage) - - outputPrinters := core.GetOutputPrinters(scanInfo, ctx, "") - - uiPrinter := core.GetUIPrinter(ctx, scanInfo, "") - - resultsHandler := resultshandling.NewResultsHandler(nil, outputPrinters, uiPrinter) - - resultsHandler.ImageScanData = []cautils.ImageScanData{ - { - PresenterConfig: scanResults, - Image: userInput, - }, - } - - resultsHandler.HandleResults(ctx) - - if imagescan.ExceedsSeverityThreshold(scanResults, failOnSeverity) { - terminateOnExceedingSeverity(scanInfo, logger.L()) - } - - return err - }, - } - - cmd.PersistentFlags().StringVarP(&imgScanInfo.Username, "username", "u", "", "Username for registry login") - cmd.PersistentFlags().StringVarP(&imgScanInfo.Password, "password", "p", "", "Password for registry login") - - return cmd -} - -// validateImageScanInfo validates the ScanInfo struct for the `image` command -func validateImageScanInfo(scanInfo *cautils.ScanInfo) error { - severity := scanInfo.FailThresholdSeverity - - if err := validateSeverity(severity); severity != "" && err != nil { - return err - } - return nil -} diff --git a/cmd/scan/scan.go b/cmd/scan/scan.go index a849ca0e..461c3a7e 100644 --- a/cmd/scan/scan.go +++ b/cmd/scan/scan.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" + "github.com/kubescape/kubescape/v2/cmd/utils" "github.com/kubescape/kubescape/v2/core/cautils" "github.com/kubescape/kubescape/v2/core/cautils/getter" "github.com/kubescape/kubescape/v2/core/meta" @@ -111,9 +112,6 @@ func GetScanCommand(ks meta.IKubescape) *cobra.Command { scanCmd.AddCommand(getFrameworkCmd(ks, &scanInfo)) scanCmd.AddCommand(getWorkloadCmd(ks, &scanInfo)) - isi := &imageScanInfo{} - scanCmd.AddCommand(getImageCmd(ks, &scanInfo, isi)) - return scanCmd } @@ -140,7 +138,7 @@ func securityScan(scanInfo cautils.ScanInfo, ks meta.IKubescape) error { return err } - enforceSeverityThresholds(results.GetData().Report.SummaryDetails.GetResourcesSeverityCounters(), &scanInfo, terminateOnExceedingSeverity) + enforceSeverityThresholds(results.GetData().Report.SummaryDetails.GetResourcesSeverityCounters(), &scanInfo, utils.TerminateOnExceedingSeverity) return nil } diff --git a/cmd/scan/scan_test.go b/cmd/scan/scan_test.go index 939765f8..12a11ebe 100644 --- a/cmd/scan/scan_test.go +++ b/cmd/scan/scan_test.go @@ -1,8 +1,6 @@ package scan import ( - "context" - "github.com/kubescape/go-logger/helpers" "github.com/kubescape/kubescape/v2/core/cautils" @@ -10,8 +8,6 @@ import ( "github.com/kubescape/opa-utils/reporthandling/apis" "github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary" - "os" - "reflect" "testing" ) @@ -176,89 +172,6 @@ func Test_enforceSeverityThresholds(t *testing.T) { } } -type spyLogMessage struct { - Message string - Details map[string]string -} - -type spyLogger struct { - setItems []spyLogMessage -} - -func (l *spyLogger) Error(msg string, details ...helpers.IDetails) {} -func (l *spyLogger) Success(msg string, details ...helpers.IDetails) {} -func (l *spyLogger) Warning(msg string, details ...helpers.IDetails) {} -func (l *spyLogger) Info(msg string, details ...helpers.IDetails) {} -func (l *spyLogger) Debug(msg string, details ...helpers.IDetails) {} -func (l *spyLogger) SetLevel(level string) error { return nil } -func (l *spyLogger) GetLevel() string { return "" } -func (l *spyLogger) SetWriter(w *os.File) {} -func (l *spyLogger) GetWriter() *os.File { return &os.File{} } -func (l *spyLogger) LoggerName() string { return "" } -func (l *spyLogger) Ctx(_ context.Context) helpers.ILogger { return l } -func (l *spyLogger) Start(msg string, details ...helpers.IDetails) {} -func (l *spyLogger) StopSuccess(msg string, details ...helpers.IDetails) {} -func (l *spyLogger) StopError(msg string, details ...helpers.IDetails) {} - -func (l *spyLogger) Fatal(msg string, details ...helpers.IDetails) { - firstDetail := details[0] - detailsMap := map[string]string{firstDetail.Key(): firstDetail.Value().(string)} - - newMsg := spyLogMessage{msg, detailsMap} - l.setItems = append(l.setItems, newMsg) -} - -func (l *spyLogger) GetSpiedItems() []spyLogMessage { - return l.setItems -} - -func Test_terminateOnExceedingSeverity(t *testing.T) { - expectedMessage := "result exceeds severity threshold" - expectedKey := "set severity threshold" - - testCases := []struct { - Description string - ExpectedMessage string - ExpectedKey string - ExpectedValue string - Logger *spyLogger - }{ - { - "Should log the Critical threshold that was set in scan info", - expectedMessage, - expectedKey, - apis.SeverityCriticalString, - &spyLogger{}, - }, - { - "Should log the High threshold that was set in scan info", - expectedMessage, - expectedKey, - apis.SeverityHighString, - &spyLogger{}, - }, - } - - for _, tc := range testCases { - t.Run( - tc.Description, - func(t *testing.T) { - want := []spyLogMessage{ - {tc.ExpectedMessage, map[string]string{tc.ExpectedKey: tc.ExpectedValue}}, - } - scanInfo := &cautils.ScanInfo{FailThresholdSeverity: tc.ExpectedValue} - - terminateOnExceedingSeverity(scanInfo, tc.Logger) - - got := tc.Logger.GetSpiedItems() - if !reflect.DeepEqual(got, want) { - t.Errorf("got: %v, want: %v", got, want) - } - }, - ) - } -} - func TestSetSecurityViewScanInfo(t *testing.T) { tests := []struct { name string diff --git a/cmd/scan/validators_test.go b/cmd/scan/validators_test.go index f3f5315c..1a22ebbc 100644 --- a/cmd/scan/validators_test.go +++ b/cmd/scan/validators_test.go @@ -96,35 +96,6 @@ func Test_validateFrameworkScanInfo(t *testing.T) { } } -func Test_validateSeverity(t *testing.T) { - testCases := []struct { - Description string - Input string - Want error - }{ - {"low should be a valid severity", "low", nil}, - {"Low should be a valid severity", "Low", nil}, - {"medium should be a valid severity", "medium", nil}, - {"Medium should be a valid severity", "Medium", nil}, - {"high should be a valid severity", "high", nil}, - {"Critical should be a valid severity", "Critical", nil}, - {"critical should be a valid severity", "critical", nil}, - {"Unknown should be an invalid severity", "Unknown", ErrUnknownSeverity}, - } - - for _, testCase := range testCases { - t.Run(testCase.Description, func(t *testing.T) { - input := testCase.Input - want := testCase.Want - got := validateSeverity(input) - - if got != want { - t.Errorf("got: %v, want: %v", got, want) - } - }) - } -} - func Test_validateWorkloadIdentifier(t *testing.T) { testCases := []struct { Description string diff --git a/cmd/utils/utils.go b/cmd/utils/utils.go new file mode 100644 index 00000000..152b6fe0 --- /dev/null +++ b/cmd/utils/utils.go @@ -0,0 +1,28 @@ +package utils + +import ( + "errors" + "fmt" + "strings" + + "github.com/kubescape/go-logger/helpers" + "github.com/kubescape/kubescape/v2/core/cautils" + reporthandlingapis "github.com/kubescape/opa-utils/reporthandling/apis" +) + +var ErrUnknownSeverity = errors.New(fmt.Sprintf("unknown severity. Supported severities are: %s", strings.Join(reporthandlingapis.GetSupportedSeverities(), ", "))) + +// validateSeverity returns an error if a given severity is not known, nil otherwise +func ValidateSeverity(severity string) error { + for _, val := range reporthandlingapis.GetSupportedSeverities() { + if strings.EqualFold(severity, val) { + return nil + } + } + return ErrUnknownSeverity + +} + +func TerminateOnExceedingSeverity(scanInfo *cautils.ScanInfo, l helpers.ILogger) { + l.Fatal("result exceeds severity threshold", helpers.String("Set severity threshold", scanInfo.FailThresholdSeverity)) +} diff --git a/cmd/utils/utils_test.go b/cmd/utils/utils_test.go new file mode 100644 index 00000000..1e482857 --- /dev/null +++ b/cmd/utils/utils_test.go @@ -0,0 +1,124 @@ +package utils + +import ( + "context" + "os" + "reflect" + "testing" + + "github.com/kubescape/go-logger/helpers" + "github.com/kubescape/kubescape/v2/core/cautils" + "github.com/kubescape/opa-utils/reporthandling/apis" +) + +type spyLogMessage struct { + Message string + Details map[string]string +} + +type spyLogger struct { + setItems []spyLogMessage +} + +func (l *spyLogger) Error(msg string, details ...helpers.IDetails) {} +func (l *spyLogger) Success(msg string, details ...helpers.IDetails) {} +func (l *spyLogger) Warning(msg string, details ...helpers.IDetails) {} +func (l *spyLogger) Info(msg string, details ...helpers.IDetails) {} +func (l *spyLogger) Debug(msg string, details ...helpers.IDetails) {} +func (l *spyLogger) SetLevel(level string) error { return nil } +func (l *spyLogger) GetLevel() string { return "" } +func (l *spyLogger) SetWriter(w *os.File) {} +func (l *spyLogger) GetWriter() *os.File { return &os.File{} } +func (l *spyLogger) LoggerName() string { return "" } +func (l *spyLogger) Ctx(_ context.Context) helpers.ILogger { return l } +func (l *spyLogger) Start(msg string, details ...helpers.IDetails) {} +func (l *spyLogger) StopSuccess(msg string, details ...helpers.IDetails) {} +func (l *spyLogger) StopError(msg string, details ...helpers.IDetails) {} + +func (l *spyLogger) Fatal(msg string, details ...helpers.IDetails) { + firstDetail := details[0] + detailsMap := map[string]string{firstDetail.Key(): firstDetail.Value().(string)} + + newMsg := spyLogMessage{msg, detailsMap} + l.setItems = append(l.setItems, newMsg) +} + +func (l *spyLogger) GetSpiedItems() []spyLogMessage { + return l.setItems +} + +func TestTerminateOnExceedingSeverity(t *testing.T) { + expectedMessage := "result exceeds severity threshold" + expectedKey := "Set severity threshold" + + testCases := []struct { + Description string + ExpectedMessage string + ExpectedKey string + ExpectedValue string + Logger *spyLogger + }{ + { + "Should log the Critical threshold that was set in scan info", + expectedMessage, + expectedKey, + apis.SeverityCriticalString, + &spyLogger{}, + }, + { + "Should log the High threshold that was set in scan info", + expectedMessage, + expectedKey, + apis.SeverityHighString, + &spyLogger{}, + }, + } + + for _, tc := range testCases { + t.Run( + tc.Description, + func(t *testing.T) { + want := []spyLogMessage{ + {tc.ExpectedMessage, map[string]string{tc.ExpectedKey: tc.ExpectedValue}}, + } + scanInfo := &cautils.ScanInfo{FailThresholdSeverity: tc.ExpectedValue} + + TerminateOnExceedingSeverity(scanInfo, tc.Logger) + + got := tc.Logger.GetSpiedItems() + if !reflect.DeepEqual(got, want) { + t.Errorf("got: %v, want: %v", got, want) + } + }, + ) + } +} + +func TestValidateSeverity(t *testing.T) { + testCases := []struct { + Description string + Input string + Want error + }{ + {"low should be a valid severity", "low", nil}, + {"Low should be a valid severity", "Low", nil}, + {"medium should be a valid severity", "medium", nil}, + {"Medium should be a valid severity", "Medium", nil}, + {"high should be a valid severity", "high", nil}, + {"Critical should be a valid severity", "Critical", nil}, + {"critical should be a valid severity", "critical", nil}, + {"Unknown should be an invalid severity", "Unknown", ErrUnknownSeverity}, + } + + for _, testCase := range testCases { + t.Run(testCase.Description, func(t *testing.T) { + input := testCase.Input + want := testCase.Want + got := ValidateSeverity(input) + + if got != want { + t.Errorf("got: %v, want: %v", got, want) + } + }) + } +} diff --git a/core/core/scan.go b/core/core/config_scan.go similarity index 100% rename from core/core/scan.go rename to core/core/config_scan.go diff --git a/core/core/image_scan.go b/core/core/image_scan.go new file mode 100644 index 00000000..2ddcd6bf --- /dev/null +++ b/core/core/image_scan.go @@ -0,0 +1,49 @@ +package core + +import ( + "context" + "fmt" + + "github.com/anchore/grype/grype/presenter/models" + logger "github.com/kubescape/go-logger" + "github.com/kubescape/kubescape/v2/core/cautils" + ksmetav1 "github.com/kubescape/kubescape/v2/core/meta/datastructures/v1" + "github.com/kubescape/kubescape/v2/core/pkg/resultshandling" + "github.com/kubescape/kubescape/v2/pkg/imagescan" +) + +func (ks *Kubescape) ScanImage(ctx context.Context, imgScanInfo *ksmetav1.ImageScanInfo, scanInfo *cautils.ScanInfo) (*models.PresenterConfig, error) { + logger.L().Start(fmt.Sprintf("Scanning image: %s", imgScanInfo.Image)) + + dbCfg, _ := imagescan.NewDefaultDBConfig() + svc := imagescan.NewScanService(dbCfg) + + creds := imagescan.RegistryCredentials{ + Username: imgScanInfo.Username, + Password: imgScanInfo.Password, + } + + scanResults, err := svc.Scan(ctx, imgScanInfo.Image, creds) + if err != nil { + logger.L().StopError(fmt.Sprintf("Failed to scan image: %s", imgScanInfo.Image)) + return nil, err + } + logger.L().StopSuccess(fmt.Sprintf("Successfully scanned image: %s", imgScanInfo.Image)) + + scanInfo.SetScanType(cautils.ScanTypeImage) + + outputPrinters := GetOutputPrinters(scanInfo, ctx, "") + + uiPrinter := GetUIPrinter(ctx, scanInfo, "") + + resultsHandler := resultshandling.NewResultsHandler(nil, outputPrinters, uiPrinter) + + resultsHandler.ImageScanData = []cautils.ImageScanData{ + { + PresenterConfig: scanResults, + Image: imgScanInfo.Image, + }, + } + + return scanResults, resultsHandler.HandleResults(ctx) +} diff --git a/core/core/patch.go b/core/core/patch.go index 2ce1c7a4..98f37b25 100644 --- a/core/core/patch.go +++ b/core/core/patch.go @@ -97,9 +97,8 @@ func (ks *Kubescape) Patch(ctx context.Context, patchInfo *ksmetav1.PatchInfo) e Image: patchedImageName, }, } - resultsHandler.HandleResults(ctx) - return nil + return resultsHandler.HandleResults(ctx) } func disableCopaLogger() { diff --git a/core/meta/datastructures/v1/image_scan.go b/core/meta/datastructures/v1/image_scan.go new file mode 100644 index 00000000..6586d2ff --- /dev/null +++ b/core/meta/datastructures/v1/image_scan.go @@ -0,0 +1,7 @@ +package v1 + +type ImageScanInfo struct { + Username string + Password string + Image string +} diff --git a/core/meta/ksinterface.go b/core/meta/ksinterface.go index 792dd788..41ea1b7b 100644 --- a/core/meta/ksinterface.go +++ b/core/meta/ksinterface.go @@ -3,6 +3,7 @@ package meta import ( "context" + "github.com/anchore/grype/grype/presenter/models" "github.com/kubescape/kubescape/v2/core/cautils" metav1 "github.com/kubescape/kubescape/v2/core/meta/datastructures/v1" "github.com/kubescape/kubescape/v2/core/pkg/resultshandling" @@ -25,4 +26,7 @@ type IKubescape interface { // patch Patch(ctx context.Context, patchInfo *metav1.PatchInfo) error + + // scan image + ScanImage(ctx context.Context, imgScanInfo *metav1.ImageScanInfo, scanInfo *cautils.ScanInfo) (*models.PresenterConfig, error) }