diff --git a/cmd/patch/patch.go b/cmd/patch/patch.go index beea56fe..91242d49 100644 --- a/cmd/patch/patch.go +++ b/cmd/patch/patch.go @@ -4,9 +4,11 @@ import ( "context" "errors" "fmt" + "strings" "time" ref "github.com/distribution/distribution/reference" + "github.com/docker/distribution/reference" "github.com/kubescape/go-logger" "github.com/kubescape/kubescape/v2/core/cautils" @@ -47,7 +49,6 @@ func GetPatchCmd(ks meta.IKubescape) *cobra.Command { 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().BoolVarP(&patchInfo.IncludeReport, "report", "r", false, "Generate & save a before and after report for the image (json format))") patchCmd.PersistentFlags().StringVarP(&patchInfo.Username, "username", "u", "", "Username for registry login") patchCmd.PersistentFlags().StringVarP(&patchInfo.Password, "password", "p", "", "Password for registry login") @@ -62,10 +63,15 @@ func validateImagePatchInfo(patchInfo *metav1.PatchInfo) error { return errors.New("image tag is required") } - // Check if image is in canonical format (required by copacetic for patching images) + // Convert image to canonical format (required by copacetic for patching images) + patchInfoImage, err := cautils.Normalize_image_name(patchInfo.Image) + if err != nil { + return nil + } + patchInfo.Image = patchInfoImage // Parse the image full name to get image name and tag named, err := ref.ParseNamed(patchInfo.Image) - if err != nil{ + if err != nil { return err } @@ -87,8 +93,18 @@ func validateImagePatchInfo(patchInfo *metav1.PatchInfo) error { } } - - patchInfo.ImageName = named.Name() + + // Extract the "image" name from the canonical Image URL + // If it's an official docker image, we store just the "image-name". Else if a docker repo then we store as "repo/image". Else complete URL + ref, _ := reference.ParseNormalizedNamed(patchInfoImage) + imageName := named.Name() + if strings.Contains(imageName, "docker.io/library/") { + imageName = reference.Path(ref) + imageName = imageName[strings.LastIndex(imageName, "/")+1:] + } else if strings.Contains(imageName, "docker.io/") { + imageName = reference.Path(ref) + } + patchInfo.ImageName = imageName return nil } diff --git a/core/pkg/opaprocessor/normalize_image_name.go b/core/cautils/normalize_image_name.go similarity index 69% rename from core/pkg/opaprocessor/normalize_image_name.go rename to core/cautils/normalize_image_name.go index 5ec12e38..bea97adf 100644 --- a/core/pkg/opaprocessor/normalize_image_name.go +++ b/core/cautils/normalize_image_name.go @@ -1,10 +1,10 @@ -package opaprocessor +package cautils import ( "github.com/docker/distribution/reference" ) -func normalize_image_name(img string) (string, error) { +func Normalize_image_name(img string) (string, error) { name, err := reference.ParseNormalizedNamed(img) if err != nil { return "", err diff --git a/core/pkg/opaprocessor/normalize_image_name_test.go b/core/cautils/normalize_image_name_test.go similarity index 85% rename from core/pkg/opaprocessor/normalize_image_name_test.go rename to core/cautils/normalize_image_name_test.go index bfb7877f..d78c91b8 100644 --- a/core/pkg/opaprocessor/normalize_image_name_test.go +++ b/core/cautils/normalize_image_name_test.go @@ -1,4 +1,4 @@ -package opaprocessor +package cautils import ( "testing" @@ -21,7 +21,7 @@ func Test_normalize_name(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - name, _ := normalize_image_name(tt.img) + name, _ := Normalize_image_name(tt.img) assert.Equal(t, tt.want, name, tt.name) }) } diff --git a/core/core/patch.go b/core/core/patch.go index 67d4c829..21d37707 100644 --- a/core/core/patch.go +++ b/core/core/patch.go @@ -17,6 +17,7 @@ import ( "github.com/kubescape/kubescape/v2/core/pkg/resultshandling" copa "github.com/project-copacetic/copacetic/pkg/patch" + log "github.com/sirupsen/logrus" ) func (ks *Kubescape) Patch(ctx context.Context, patchInfo *ksmetav1.PatchInfo) error { @@ -52,40 +53,37 @@ func (ks *Kubescape) Patch(ctx context.Context, patchInfo *ksmetav1.PatchInfo) e // ===================== Patch the image using copacetic ===================== logger.L().Start("Patching image...") + + sout, serr := os.Stdout, os.Stderr + if logger.L().GetLevel() != "debug" { + disableCopaLogger() + } + if err := copa.Patch(ctx, patchInfo.Timeout, patchInfo.BuildkitAddress, patchInfo.Image, fileName, patchInfo.PatchedImageTag, ""); err != nil { return err } - logger.L().StopSuccess("Patched image successfully") + + if logger.L().GetLevel() != "debug" { + enableCopaLogger(sout, serr) + } + + patchedImageName := fmt.Sprintf("%s:%s", patchInfo.ImageName, patchInfo.PatchedImageTag) + logger.L().StopSuccess(fmt.Sprintf("Patched image successfully. Loaded image: %s", patchedImageName)) // ===================== Re-scan the image ===================== - // Re-scan the image - patchedImageName := fmt.Sprintf("%s:%s", patchInfo.ImageName, patchInfo.PatchedImageTag) logger.L().Start(fmt.Sprintf("Re-Scanning image: %s", patchedImageName)) scanResultsPatched, err := svc.Scan(ctx, patchedImageName, creds) if err != nil { return err } - // Save the patched image's scan results to a file in json format, if requested - fileNamePatched := fmt.Sprintf("%s:%s.json", patchInfo.ImageName, patchInfo.PatchedImageTag) - fileNamePatched = strings.ReplaceAll(fileNamePatched, "/", "-") - - if patchInfo.IncludeReport { - pres = presenter.GetPresenter("json", "", false, *scanResultsPatched) - writer = printer.GetWriter(ctx, fileNamePatched) - if err := pres.Present(writer); err != nil { - return err - } - } logger.L().StopSuccess(fmt.Sprintf("Successfully re-scanned image: %s", patchedImageName)) // ===================== Clean up ===================== - // Remove the scan results files, which were used to patch the image - if !patchInfo.IncludeReport { - if err := os.Remove(fileName); err != nil { - logger.L().Warning(fmt.Sprintf("failed to remove residual file: %v", fileName), helpers.Error(err)) - } + // Remove the scan results file, which was used to patch the image + if err := os.Remove(fileName); err != nil { + logger.L().Warning(fmt.Sprintf("failed to remove residual file: %v", fileName), helpers.Error(err)) } // ===================== Results Handling ===================== @@ -103,11 +101,16 @@ func (ks *Kubescape) Patch(ctx context.Context, patchInfo *ksmetav1.PatchInfo) e } resultsHandler.HandleResults(ctx) - if patchInfo.IncludeReport { - logger.L().Success("Results saved", helpers.String("filename", fileName)) - logger.L().Success("Results saved", helpers.String("filename", fileNamePatched)) - } - return nil - +} + +func disableCopaLogger() { + os.Stdout, os.Stderr = nil, nil + null, _ := os.Open(os.DevNull) + log.SetOutput(null) +} + +func enableCopaLogger(sout, serr *os.File) { + os.Stdout, os.Stderr = sout, serr + log.SetOutput(os.Stdout) } diff --git a/core/meta/datastructures/v1/patch.go b/core/meta/datastructures/v1/patch.go index a1723f30..793d1531 100644 --- a/core/meta/datastructures/v1/patch.go +++ b/core/meta/datastructures/v1/patch.go @@ -7,9 +7,8 @@ type PatchInfo struct { PatchedImageTag string // can be empty, if empty then the image tag will be patched with the latest tag BuildkitAddress string // buildkit address Timeout time.Duration // timeout for patching an image - IncludeReport bool // include report in the output - - // Image registry credentials + + // Image registry credentials Username string // username for registry login Password string // password for registry login diff --git a/core/pkg/opaprocessor/utils.go b/core/pkg/opaprocessor/utils.go index 2e7efd01..a38189f5 100644 --- a/core/pkg/opaprocessor/utils.go +++ b/core/pkg/opaprocessor/utils.go @@ -106,6 +106,6 @@ var imageNameNormalizeDefinition = func(bctx rego.BuiltinContext, a *ast.Term) ( if err != nil { return nil, fmt.Errorf("invalid parameter type: %v", err) } - normalizedName, err := normalize_image_name(string(aStr)) + normalizedName, err := cautils.Normalize_image_name(string(aStr)) return ast.StringTerm(normalizedName), err } diff --git a/core/pkg/resultshandling/printer/v2/jsonprinter.go b/core/pkg/resultshandling/printer/v2/jsonprinter.go index 1cb36d41..ee629026 100644 --- a/core/pkg/resultshandling/printer/v2/jsonprinter.go +++ b/core/pkg/resultshandling/printer/v2/jsonprinter.go @@ -75,7 +75,6 @@ func printConfigurationsScanning(opaSessionObj *cautils.OPASessionObj, ctx conte } func (jp *JsonPrinter) PrintImageScan(ctx context.Context, scanResults *models.PresenterConfig) error { - // presenterConfig, _ := presenter.ValidatedConfig("json", "", false) pres := presenter.GetPresenter("json", "", false, *scanResults) return pres.Present(jp.writer) diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter/imagescan.go b/core/pkg/resultshandling/printer/v2/prettyprinter/imagescan.go index f91c5ea3..8adbb32d 100644 --- a/core/pkg/resultshandling/printer/v2/prettyprinter/imagescan.go +++ b/core/pkg/resultshandling/printer/v2/prettyprinter/imagescan.go @@ -48,8 +48,8 @@ func (ip *ImagePrinter) PrintConfigurationsScanning(summaryDetails *reportsummar func (ip *ImagePrinter) PrintNextSteps() { if ip.verboseMode { - printNextSteps(ip.writer, []string{CICDSetupText, installHelmText, imagePatchText}, true) + printNextSteps(ip.writer, []string{CICDSetupText, installHelmText}, true) return } - printNextSteps(ip.writer, []string{imageScanVerboseRunText, CICDSetupText, installHelmText, imagePatchText}, true) + printNextSteps(ip.writer, []string{imageScanVerboseRunText, CICDSetupText, installHelmText}, true) } diff --git a/core/pkg/resultshandling/printer/v2/prettyprinter/utils.go b/core/pkg/resultshandling/printer/v2/prettyprinter/utils.go index 7bfdf8bb..fcb262e8 100644 --- a/core/pkg/resultshandling/printer/v2/prettyprinter/utils.go +++ b/core/pkg/resultshandling/printer/v2/prettyprinter/utils.go @@ -27,7 +27,6 @@ var ( clusterScanRunText = fmt.Sprintf("Run a cluster scan: %s", getCallToActionString("'$ kubescape scan'")) installHelmText = fmt.Sprintf("Install Kubescape in your cluster for continuous monitoring: %s", linkToHelm) CICDSetupText = fmt.Sprintf("Add Kubescape to your CI/CD pipeline: %s", linkToCICDSetup) - imagePatchText = fmt.Sprintf("Try patching your images with: %s", getCallToActionString("'$ kubescape patch'")) complianceFrameworks = []string{"nsa", "mitre"} cveSeverities = []string{"Critical", "High", "Medium", "Low", "Negligible", "Unknown"} ) diff --git a/go.mod b/go.mod index 2f8aecfd..ee61036d 100644 --- a/go.mod +++ b/go.mod @@ -38,6 +38,7 @@ require ( github.com/schollz/progressbar/v3 v3.13.0 github.com/sergi/go-diff v1.3.1 github.com/sigstore/cosign/v2 v2.1.1 + github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.7.0 github.com/stretchr/testify v1.8.4 github.com/whilp/git-urls v1.0.0 @@ -343,7 +344,6 @@ require ( github.com/sigstore/rekor v1.2.2-0.20230530122220-67cc9e58bd23 // indirect github.com/sigstore/sigstore v1.7.1 // indirect github.com/sigstore/timestamp-authority v1.1.1 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.2.0 // indirect github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect github.com/spdx/tools-golang v0.5.3 // indirect