diff --git a/pkg/patch/cmd.go b/pkg/patch/cmd.go index 1e6a59bd..0ef3e50a 100644 --- a/pkg/patch/cmd.go +++ b/pkg/patch/cmd.go @@ -21,15 +21,16 @@ import ( ) type patchArgs struct { - appImage string - reportFile string - patchedTag string - workingFolder string - timeout time.Duration - ignoreError bool - format string - output string - bkOpts buildkit.Opts + appImage string + reportFile string + patchedTag string + workingFolder string + timeout time.Duration + ignoreError bool + format string + output string + customToolingImage string + bkOpts buildkit.Opts } func NewPatchCmd() *cobra.Command { @@ -53,6 +54,7 @@ func NewPatchCmd() *cobra.Command { ua.workingFolder, ua.format, ua.output, + ua.customToolingImage, ua.ignoreError, bkopts) }, @@ -70,6 +72,7 @@ func NewPatchCmd() *cobra.Command { flags.BoolVar(&ua.ignoreError, "ignore-errors", false, "Ignore errors and continue patching") flags.StringVarP(&ua.format, "format", "f", "openvex", "Output format, defaults to 'openvex'") flags.StringVarP(&ua.output, "output", "o", "", "Output file path") + flags.StringVarP(&ua.customToolingImage, "custom-tooling-image", "", "", "[EXPERIMENTAL] Custom tooling image to use for patching") if err := patchCmd.MarkFlagRequired("image"); err != nil { panic(err) diff --git a/pkg/patch/patch.go b/pkg/patch/patch.go index 07b7081b..c2d3150b 100644 --- a/pkg/patch/patch.go +++ b/pkg/patch/patch.go @@ -29,13 +29,13 @@ const ( ) // Patch command applies package updates to an OCI image given a vulnerability report. -func Patch(ctx context.Context, timeout time.Duration, image, reportFile, patchedTag, workingFolder, format, output string, ignoreError bool, bkOpts buildkit.Opts) error { +func Patch(ctx context.Context, timeout time.Duration, image, reportFile, patchedTag, workingFolder, format, output, customToolingImage string, ignoreError bool, bkOpts buildkit.Opts) error { timeoutCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() ch := make(chan error) go func() { - ch <- patchWithContext(timeoutCtx, image, reportFile, patchedTag, workingFolder, format, output, ignoreError, bkOpts) + ch <- patchWithContext(timeoutCtx, image, reportFile, patchedTag, workingFolder, format, output, customToolingImage, ignoreError, bkOpts) }() select { @@ -60,7 +60,7 @@ func removeIfNotDebug(workingFolder string) { } } -func patchWithContext(ctx context.Context, image, reportFile, patchedTag, workingFolder, format, output string, ignoreError bool, bkOpts buildkit.Opts) error { +func patchWithContext(ctx context.Context, image, reportFile, patchedTag, workingFolder, format, output, customToolingImage string, ignoreError bool, bkOpts buildkit.Opts) error { imageName, err := ref.ParseNamed(image) if err != nil { return err @@ -133,7 +133,7 @@ func patchWithContext(ctx context.Context, image, reportFile, patchedTag, workin // Export the patched image state to Docker // TODO: Add support for other output modes as buildctl does. - patchedImageState, errPkgs, err := pkgmgr.InstallUpdates(ctx, updates, ignoreError) + patchedImageState, errPkgs, err := pkgmgr.InstallUpdates(ctx, updates, ignoreError, customToolingImage) if err != nil { return err } diff --git a/pkg/pkgmgr/apk.go b/pkg/pkgmgr/apk.go index 4e521216..fa5c5399 100644 --- a/pkg/pkgmgr/apk.go +++ b/pkg/pkgmgr/apk.go @@ -124,7 +124,7 @@ func validateAPKPackageVersions(updates types.UpdatePackages, cmp VersionCompare return errorPkgs, allErrors.ErrorOrNil() } -func (am *apkManager) InstallUpdates(ctx context.Context, manifest *types.UpdateManifest, ignoreErrors bool) (*llb.State, []string, error) { +func (am *apkManager) InstallUpdates(ctx context.Context, manifest *types.UpdateManifest, ignoreErrors bool, _ string) (*llb.State, []string, error) { // Resolve set of unique packages to update apkComparer := VersionComparer{isValidAPKVersion, isLessThanAPKVersion} updates, err := GetUniqueLatestUpdates(manifest.Updates, apkComparer, ignoreErrors) diff --git a/pkg/pkgmgr/dpkg.go b/pkg/pkgmgr/dpkg.go index 3287ba80..c30b35ff 100644 --- a/pkg/pkgmgr/dpkg.go +++ b/pkg/pkgmgr/dpkg.go @@ -72,7 +72,11 @@ func isLessThanDebianVersion(v1, v2 string) bool { } // Map the target image OSType & OSVersion to an appropriate tooling image. -func getAPTImageName(manifest *types.UpdateManifest) string { +func getAPTImageName(manifest *types.UpdateManifest, customToolingImage string) string { + if customToolingImage != "" { + return customToolingImage + } + version := manifest.OSVersion if manifest.OSType == "debian" { version = strings.Split(manifest.OSVersion, ".")[0] + "-slim" @@ -98,7 +102,7 @@ func getDPKGStatusType(dir string) dpkgStatusType { return out } -func (dm *dpkgManager) InstallUpdates(ctx context.Context, manifest *types.UpdateManifest, ignoreErrors bool) (*llb.State, []string, error) { +func (dm *dpkgManager) InstallUpdates(ctx context.Context, manifest *types.UpdateManifest, ignoreErrors bool, customToolingImage string) (*llb.State, []string, error) { // Validate and extract unique updates listed in input manifest debComparer := VersionComparer{isValidDebianVersion, isLessThanDebianVersion} updates, err := GetUniqueLatestUpdates(manifest.Updates, debComparer, ignoreErrors) @@ -111,7 +115,7 @@ func (dm *dpkgManager) InstallUpdates(ctx context.Context, manifest *types.Updat } // Probe for additional information to execute the appropriate update install graphs - toolImageName := getAPTImageName(manifest) + toolImageName := getAPTImageName(manifest, customToolingImage) if err := dm.probeDPKGStatus(ctx, toolImageName); err != nil { return nil, nil, err } diff --git a/pkg/pkgmgr/dpkg_test.go b/pkg/pkgmgr/dpkg_test.go index 0798d6b4..4d6a15da 100644 --- a/pkg/pkgmgr/dpkg_test.go +++ b/pkg/pkgmgr/dpkg_test.go @@ -119,10 +119,16 @@ func TestGetAPTImageName(t *testing.T) { // Run test cases for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - got := getAPTImageName(tc.manifest) + got := getAPTImageName(tc.manifest, "") if got != tc.want { t.Errorf("getAPTImageName() = %v, want %v", got, tc.want) } + + // Test with custom tooling image + got = getAPTImageName(tc.manifest, "docker.io/foo:bar") + if got != "docker.io/foo:bar" { + t.Errorf("getAPTImageName() = %v, want %v", got, "docker.io/foo:bar") + } }) } } diff --git a/pkg/pkgmgr/pkgmgr.go b/pkg/pkgmgr/pkgmgr.go index a88c565d..73f42f1d 100644 --- a/pkg/pkgmgr/pkgmgr.go +++ b/pkg/pkgmgr/pkgmgr.go @@ -27,7 +27,7 @@ const ( ) type PackageManager interface { - InstallUpdates(context.Context, *types.UpdateManifest, bool) (*llb.State, []string, error) + InstallUpdates(context.Context, *types.UpdateManifest, bool, string) (*llb.State, []string, error) GetPackageType() string } diff --git a/pkg/pkgmgr/rpm.go b/pkg/pkgmgr/rpm.go index f4d5287e..0ac71f4c 100644 --- a/pkg/pkgmgr/rpm.go +++ b/pkg/pkgmgr/rpm.go @@ -90,7 +90,11 @@ func isLessThanRPMVersion(v1, v2 string) bool { } // Map the target image OSType & OSVersion to an appropriate tooling image. -func getRPMImageName(manifest *types.UpdateManifest) string { +func getRPMImageName(manifest *types.UpdateManifest, customToolingImage string) string { + if customToolingImage != "" { + return customToolingImage + } + // Standardize on mariner as tooling image base as redhat/ubi does not provide // static busybox binary image := "mcr.microsoft.com/cbl-mariner/base/core" @@ -159,7 +163,7 @@ func getRPMDBType(dir string) rpmDBType { return out } -func (rm *rpmManager) InstallUpdates(ctx context.Context, manifest *types.UpdateManifest, ignoreErrors bool) (*llb.State, []string, error) { +func (rm *rpmManager) InstallUpdates(ctx context.Context, manifest *types.UpdateManifest, ignoreErrors bool, customToolingImage string) (*llb.State, []string, error) { // Resolve set of unique packages to update rpmComparer := VersionComparer{isValidRPMVersion, isLessThanRPMVersion} updates, err := GetUniqueLatestUpdates(manifest.Updates, rpmComparer, ignoreErrors) @@ -173,7 +177,7 @@ func (rm *rpmManager) InstallUpdates(ctx context.Context, manifest *types.Update log.Debugf("latest unique RPMs: %v", updates) // Probe RPM status for available tooling on the target image - toolImageName := getRPMImageName(manifest) + toolImageName := getRPMImageName(manifest, customToolingImage) if err := rm.probeRPMStatus(ctx, toolImageName); err != nil { return nil, nil, err } diff --git a/pkg/pkgmgr/rpm_test.go b/pkg/pkgmgr/rpm_test.go index eb83cf64..bee89afe 100644 --- a/pkg/pkgmgr/rpm_test.go +++ b/pkg/pkgmgr/rpm_test.go @@ -133,10 +133,12 @@ func TestGetRPMImageName(t *testing.T) { // Loop over test cases and run getRPMImageName function with each input manifest for _, tc := range testCases { t.Run(tc.image, func(t *testing.T) { - image := getRPMImageName(tc.manifest) - - // Use testify package to assert that the output image name matches the expected one + image := getRPMImageName(tc.manifest, "") assert.Equal(t, tc.image, image) + + // Test with custom tooling image + image = getRPMImageName(tc.manifest, "docker.io/foo:bar") + assert.Equal(t, "docker.io/foo:bar", image) }) } } diff --git a/website/docs/faq.md b/website/docs/faq.md index 479c4ddb..0f8f431f 100644 --- a/website/docs/faq.md +++ b/website/docs/faq.md @@ -3,4 +3,18 @@ title: FAQ --- ## What kind of vulnerabilities can Copa patch? -Copa is capable of patching "OS level" vulnerabilities. This includes packages (like `openssl`) in the image that are managed by a package manager such as `apt` or `yum`. Copa is not currently capable of patching vulnerabilities at the "application level" such as Python packages or Go modules. \ No newline at end of file +Copa is capable of patching "OS level" vulnerabilities. This includes packages (like `openssl`) in the image that are managed by a package manager such as `apt` or `yum`. Copa is not currently capable of patching vulnerabilities at the "application level" such as Python packages or Go modules. + +## Can I replace the package repositories in the image with my own? + +:::caution + +Experimental: This feature might change without preserving backwards compatibility. + +::: + +Copa does not support replacing the repositories in the package managers with alternatives. Images must already use the intended package repositories. For example, for debian, updating `/etc/apt/sources.list` from `http://archive.ubuntu.com/ubuntu/` to a mirror, such as `https://mirrors.wikimedia.org/ubuntu/`. If you need the tooling image to use a different package repository, you can create a custom tooling image with the desired package repository and specify it using `--custom-tooling-image` flag. + +```shell +copa patch --image docker.io/myorg/image:tag --custom-tooling-image docker.io/myorg/tooling-base:tag ... +```