From 46a1db29f4ca8d9cfc3d916a5907e6d99fa882d7 Mon Sep 17 00:00:00 2001 From: Miaha Cybersec Date: Mon, 29 Jul 2024 16:15:20 -0600 Subject: [PATCH 01/10] Refactor and test patched tag generation Signed-off-by: Miaha Cybersec --- pkg/patch/patch.go | 63 +++++++++++++++++++++++++++-------------- pkg/patch/patch_test.go | 40 ++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 22 deletions(-) diff --git a/pkg/patch/patch.go b/pkg/patch/patch.go index 24f69a80a..4082b3f94 100644 --- a/pkg/patch/patch.go +++ b/pkg/patch/patch.go @@ -74,35 +74,26 @@ func removeIfNotDebug(workingFolder string) { } } -func patchWithContext(ctx context.Context, ch chan error, image, reportFile, patchedTag, workingFolder, scanner, format, output string, ignoreError bool, bkOpts buildkit.Opts) error { - imageName, err := reference.ParseNormalizedNamed(image) +// patchWithContext patches the user-supplied image, image +func patchWithContext(ctx context.Context, ch chan error, image, reportFile, userSuppliedPatchTag, workingFolder, scanner, format, output string, ignoreError bool, bkOpts buildkit.Opts) error { + dockerNormalizedImageName, err := reference.ParseNormalizedNamed(image) if err != nil { return err } - if reference.IsNameOnly(imageName) { + + if reference.IsNameOnly(dockerNormalizedImageName) { log.Warnf("Image name has no tag or digest, using latest as tag") - imageName = reference.TagNameOnly(imageName) - } - var tag string - taggedName, ok := imageName.(reference.Tagged) - if ok { - tag = taggedName.Tag() - } else { - log.Warnf("Image name has no tag") - } - if patchedTag == "" { - if tag == "" { - log.Warnf("No output tag specified for digest-referenced image, defaulting to `%s`", defaultPatchedTagSuffix) - patchedTag = defaultPatchedTagSuffix - } else { - patchedTag = fmt.Sprintf("%s-%s", tag, defaultPatchedTagSuffix) - } + dockerNormalizedImageName = reference.TagNameOnly(dockerNormalizedImageName) } - _, err = reference.WithTag(imageName, patchedTag) + + patchedTag := generatePatchedTag(dockerNormalizedImageName, userSuppliedPatchTag) + + _, err = reference.WithTag(dockerNormalizedImageName, patchedTag) if err != nil { return fmt.Errorf("%w with patched tag %s", err, patchedTag) } - patchedImageName := fmt.Sprintf("%s:%s", imageName.Name(), patchedTag) + + patchedImageName := fmt.Sprintf("%s:%s", dockerNormalizedImageName.Name(), patchedTag) // Ensure working folder exists for call to InstallUpdates if workingFolder == "" { @@ -168,7 +159,7 @@ func patchWithContext(ctx context.Context, ch chan error, image, reportFile, pat eg.Go(func() error { _, err := bkClient.Build(ctx, solveOpt, copaProduct, func(ctx context.Context, c gwclient.Client) (*gwclient.Result, error) { // Configure buildctl/client for use by package manager - config, err := buildkit.InitializeBuildkitConfig(ctx, c, imageName.String()) + config, err := buildkit.InitializeBuildkitConfig(ctx, c, dockerNormalizedImageName.String()) if err != nil { ch <- err return nil, err @@ -301,6 +292,34 @@ func patchWithContext(ctx context.Context, ch chan error, image, reportFile, pat return eg.Wait() } +func generatePatchedTag(dockerNormalizedImageName reference.Named, userSuppliedPatchTag string) string { + // officialTag is typically the versioning tag of the image as published in a container registry + var officialTag string + var copaTag string + + taggedName, ok := dockerNormalizedImageName.(reference.Tagged) + + if ok { + officialTag = taggedName.Tag() + } else { + log.Warnf("Image name has no tag") + } + + if userSuppliedPatchTag != "" { + copaTag = fmt.Sprintf("%s-%s", officialTag, userSuppliedPatchTag) + return copaTag + } else { + if officialTag == "" { + log.Warnf("No output tag specified for digest-referenced image, defaulting to `%s`", defaultPatchedTagSuffix) + copaTag = defaultPatchedTagSuffix + } else { + copaTag = fmt.Sprintf("%s-%s", officialTag, defaultPatchedTagSuffix) + } + } + + return copaTag +} + func getOSType(ctx context.Context, osreleaseBytes []byte) (string, error) { r := bytes.NewReader(osreleaseBytes) osData, err := osrelease.Parse(ctx, r) diff --git a/pkg/patch/patch_test.go b/pkg/patch/patch_test.go index ad54c34bc..0fa776eec 100644 --- a/pkg/patch/patch_test.go +++ b/pkg/patch/patch_test.go @@ -6,6 +6,8 @@ import ( "os" "testing" + "github.com/distribution/reference" + log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" ) @@ -248,3 +250,41 @@ func TestGetOSVersion(t *testing.T) { }) } } + +func TestGeneratePatchedTag(t *testing.T) { + testCases := []struct { + name string + dockerImageName string + userSuppliedPatchTag string + expectedPatchedTag string + }{ + { + name: "NoTag_NoUserSupplied", + dockerImageName: "docker.io/library/alpine", + userSuppliedPatchTag: "", + expectedPatchedTag: defaultPatchedTagSuffix, + }, + { + name: "WithTag_NoUserSupplied", + dockerImageName: "docker.io/redhat/ubi9:latest", + userSuppliedPatchTag: "", + expectedPatchedTag: "latest-patched", + }, + { + name: "WithTag_UserSupplied", + dockerImageName: "docker.io/librari/ubuntu:jammy-20231004", + userSuppliedPatchTag: "01", + expectedPatchedTag: "jammy-20231004-01", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + named, _ := reference.ParseNormalizedNamed(tc.dockerImageName) + patchedTag := generatePatchedTag(named, tc.userSuppliedPatchTag) + if patchedTag != tc.expectedPatchedTag { + t.Errorf("expected patchedTag to be %s but got %s", tc.expectedPatchedTag, patchedTag) + } + }) + } +} From d22811e03721e5e4c41470f80e9b8b04fd864faa Mon Sep 17 00:00:00 2001 From: Miaha Cybersec Date: Tue, 30 Jul 2024 11:05:22 -0600 Subject: [PATCH 02/10] Refactor patch processing workflow Signed-off-by: Miaha Cybersec --- pkg/patch/patch.go | 236 +++++++++++++++++++++++---------------------- pkg/pkgmgr/apk.go | 4 - 2 files changed, 121 insertions(+), 119 deletions(-) diff --git a/pkg/patch/patch.go b/pkg/patch/patch.go index a6fff3765..5eefb2cf9 100644 --- a/pkg/patch/patch.go +++ b/pkg/patch/patch.go @@ -74,7 +74,7 @@ func removeIfNotDebug(workingFolder string) { } } -// patchWithContext patches the user-supplied image, image +// patchWithContext patches the user-supplied image, image. func patchWithContext(ctx context.Context, ch chan error, image, reportFile, userSuppliedPatchTag, workingFolder, scanner, format, output string, ignoreError bool, bkOpts buildkit.Opts) error { dockerNormalizedImageName, err := reference.ParseNormalizedNamed(image) if err != nil { @@ -157,113 +157,7 @@ func patchWithContext(ctx context.Context, ch chan error, image, reportFile, use buildChannel := make(chan *client.SolveStatus) eg, ctx := errgroup.WithContext(ctx) eg.Go(func() error { - _, err := bkClient.Build(ctx, solveOpt, copaProduct, func(ctx context.Context, c gwclient.Client) (*gwclient.Result, error) { - // Configure buildctl/client for use by package manager - config, err := buildkit.InitializeBuildkitConfig(ctx, c, dockerNormalizedImageName.String()) - if err != nil { - ch <- err - return nil, err - } - - // Create package manager helper - var manager pkgmgr.PackageManager - if reportFile == "" { - // determine OS family - fileBytes, err := buildkit.ExtractFileFromState(ctx, c, &config.ImageState, "/etc/os-release") - if err != nil { - ch <- err - return nil, fmt.Errorf("unable to extract /etc/os-release file from state %w", err) - } - - osType, err := getOSType(ctx, fileBytes) - if err != nil { - ch <- err - return nil, err - } - - osVersion, err := getOSVersion(ctx, fileBytes) - if err != nil { - ch <- err - return nil, err - } - - // get package manager based on os family type - manager, err = pkgmgr.GetPackageManager(osType, osVersion, config, workingFolder) - if err != nil { - ch <- err - return nil, err - } - } else { - // get package manager based on os family type - manager, err = pkgmgr.GetPackageManager(updates.Metadata.OS.Type, updates.Metadata.OS.Version, config, workingFolder) - if err != nil { - ch <- err - return nil, err - } - } - - // Export the patched image state to Docker - // TODO: Add support for other output modes as buildctl does. - patchedImageState, errPkgs, err := manager.InstallUpdates(ctx, updates, ignoreError) - if err != nil { - ch <- err - return nil, err - } - - platform := platforms.Normalize(platforms.DefaultSpec()) - if platform.OS != "linux" { - platform.OS = "linux" - } - - def, err := patchedImageState.Marshal(ctx, llb.Platform(platform)) - if err != nil { - ch <- err - return nil, fmt.Errorf("unable to get platform from ImageState %w", err) - } - - res, err := c.Solve(ctx, gwclient.SolveRequest{ - Definition: def.ToPB(), - Evaluate: true, - }) - if err != nil { - ch <- err - return nil, err - } - - res.AddMeta(exptypes.ExporterImageConfigKey, config.ConfigData) - - // Currently can only validate updates if updating via scanner - if reportFile != "" { - // create a new manifest with the successfully patched packages - validatedManifest := &unversioned.UpdateManifest{ - Metadata: unversioned.Metadata{ - OS: unversioned.OS{ - Type: updates.Metadata.OS.Type, - Version: updates.Metadata.OS.Version, - }, - Config: unversioned.Config{ - Arch: updates.Metadata.Config.Arch, - }, - }, - Updates: []unversioned.UpdatePackage{}, - } - for _, update := range updates.Updates { - if !slices.Contains(errPkgs, update.Name) { - validatedManifest.Updates = append(validatedManifest.Updates, update) - } - } - // vex document must contain at least one statement - if output != "" && len(validatedManifest.Updates) > 0 { - if err := vex.TryOutputVexDocument(validatedManifest, manager, patchedImageName, format, output); err != nil { - ch <- err - return nil, err - } - } - } - - return res, nil - }, buildChannel) - + err = buildkitBuild(bkClient, ctx, &solveOpt, dockerNormalizedImageName, ch, reportFile, workingFolder, updates, ignoreError, output, patchedImageName, format, buildChannel) return err }) @@ -292,6 +186,120 @@ func patchWithContext(ctx context.Context, ch chan error, image, reportFile, use return eg.Wait() } +func buildkitBuild(bkClient *client.Client, ctx context.Context, solveOpt *client.SolveOpt, dockerNormalizedImageName reference.Named, ch chan error, reportFile string, workingFolder string, updates *unversioned.UpdateManifest, ignoreError bool, output string, patchedImageName string, format string, buildChannel chan *client.SolveStatus) error { + _, err := bkClient.Build(ctx, *solveOpt, copaProduct, func(ctx context.Context, c gwclient.Client) (*gwclient.Result, error) { + bkConfig, err := buildkit.InitializeBuildkitConfig(ctx, c, dockerNormalizedImageName.String()) + if err != nil { + return handleError(ch, err) + } + + manager, err := resolvePackageManager(ctx, c, bkConfig, reportFile, updates, workingFolder) + if err != nil { + return handleError(ch, err) + } + + return buildReport(ctx, ch, bkConfig, manager, updates, ignoreError, patchedImageName, format, output, reportFile) + }, buildChannel) + return err +} + +func resolvePackageManager(ctx context.Context, c gwclient.Client, config *buildkit.Config, reportFile string, updates *unversioned.UpdateManifest, workingFolder string) (pkgmgr.PackageManager, error) { + var manager pkgmgr.PackageManager + if reportFile == "" { + fileBytes, err := buildkit.ExtractFileFromState(ctx, c, &config.ImageState, "/etc/os-release") + if err != nil { + return nil, err + } + + osType, err := getOSType(ctx, fileBytes) + if err != nil { + return nil, err + } + + osVersion, err := getOSVersion(ctx, fileBytes) + if err != nil { + return nil, err + } + // get package manager based on os family type + manager, err = pkgmgr.GetPackageManager(osType, osVersion, config, workingFolder) + if err != nil { + return nil, err + } + } else { + // get package manager based on os family type + var err error + manager, err = pkgmgr.GetPackageManager(updates.Metadata.OS.Type, updates.Metadata.OS.Version, config, workingFolder) + if err != nil { + return nil, err + } + } + return manager, nil +} + +// handleError streamlines error forwarding to error channel and returns the error again for further propagation. +func handleError(ch chan error, err error) (*gwclient.Result, error) { + ch <- err + return nil, err +} + +// buildReport is an extracted method containing logic to manage the updates and build report. +func buildReport(ctx context.Context, ch chan error, config *buildkit.Config, manager pkgmgr.PackageManager, updates *unversioned.UpdateManifest, ignoreError bool, patchedImageName string, format string, output string, reportFile string) (*gwclient.Result, error) { + patchedImageState, errPkgs, err := manager.InstallUpdates(ctx, updates, ignoreError) + if err != nil { + return handleError(ch, err) + } + platform := platforms.Normalize(platforms.DefaultSpec()) + if platform.OS != "linux" { + platform.OS = "linux" + } + def, err := patchedImageState.Marshal(ctx, llb.Platform(platform)) + if err != nil { + return handleError(ch, fmt.Errorf("unable to get platform from ImageState %w", err)) + } + res, err := config.Client.Solve(ctx, gwclient.SolveRequest{ + Definition: def.ToPB(), + Evaluate: true, + }) + if err != nil { + return handleError(ch, err) + } + res.AddMeta(exptypes.ExporterImageConfigKey, config.ConfigData) + // Currently can only validate updates if updating via scanner + if reportFile != "" { + validatedManifest := updateManifest(updates, errPkgs) + // vex document must contain at least one statement + if output != "" && len(validatedManifest.Updates) > 0 { + err = vex.TryOutputVexDocument(validatedManifest, manager, patchedImageName, format, output) + if err != nil { + return handleError(ch, err) + } + } + } + return res, nil +} + +// updateManifest creates a new manifest with the successfully patched packages. +func updateManifest(updates *unversioned.UpdateManifest, errPkgs []string) *unversioned.UpdateManifest { + validatedManifest := &unversioned.UpdateManifest{ + Metadata: unversioned.Metadata{ + OS: unversioned.OS{ + Type: updates.Metadata.OS.Type, + Version: updates.Metadata.OS.Version, + }, + Config: unversioned.Config{ + Arch: updates.Metadata.Config.Arch, + }, + }, + Updates: []unversioned.UpdatePackage{}, + } + for _, update := range updates.Updates { + if !slices.Contains(errPkgs, update.Name) { + validatedManifest.Updates = append(validatedManifest.Updates, update) + } + } + return validatedManifest +} + func generatePatchedTag(dockerNormalizedImageName reference.Named, userSuppliedPatchTag string) string { // officialTag is typically the versioning tag of the image as published in a container registry var officialTag string @@ -308,15 +316,13 @@ func generatePatchedTag(dockerNormalizedImageName reference.Named, userSuppliedP if userSuppliedPatchTag != "" { copaTag = fmt.Sprintf("%s-%s", officialTag, userSuppliedPatchTag) return copaTag - } else { - if officialTag == "" { - log.Warnf("No output tag specified for digest-referenced image, defaulting to `%s`", defaultPatchedTagSuffix) - copaTag = defaultPatchedTagSuffix - } else { - copaTag = fmt.Sprintf("%s-%s", officialTag, defaultPatchedTagSuffix) - } + } else if officialTag == "" { + log.Warnf("No output tag specified for digest-referenced image, defaulting to `%s`", defaultPatchedTagSuffix) + copaTag = defaultPatchedTagSuffix + return copaTag } + copaTag = fmt.Sprintf("%s-%s", officialTag, defaultPatchedTagSuffix) return copaTag } diff --git a/pkg/pkgmgr/apk.go b/pkg/pkgmgr/apk.go index 08cf26bb8..7200d94ba 100644 --- a/pkg/pkgmgr/apk.go +++ b/pkg/pkgmgr/apk.go @@ -157,10 +157,6 @@ func (am *apkManager) InstallUpdates(ctx context.Context, manifest *unversioned. // Patch a regular alpine image with: // - sh and apk installed on the image // - valid apk db state on the image -// -// TODO: support "distroless" Alpine images (e.g. APKO images) -// Still assumes that APK exists in the target image and is pathed, which can be addressed by -// mounting a copy of apk-tools-static into the image and invoking apk-static directly. func (am *apkManager) upgradePackages(ctx context.Context, updates unversioned.UpdatePackages, ignoreErrors bool) (*llb.State, []byte, error) { // TODO: Add support for custom APK config apkUpdated := am.config.ImageState.Run(llb.Shlex("apk update"), llb.WithProxy(utils.GetProxy()), llb.IgnoreCache).Root() From 6183f0b0edf83b30ada6c7b008e778d4f4078fd3 Mon Sep 17 00:00:00 2001 From: Miaha Cybersec Date: Tue, 30 Jul 2024 13:48:39 -0600 Subject: [PATCH 03/10] Refactor build functions for improved readability Signed-off-by: Miaha Cybersec --- pkg/patch/patch.go | 62 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/pkg/patch/patch.go b/pkg/patch/patch.go index 5eefb2cf9..a952a7c45 100644 --- a/pkg/patch/patch.go +++ b/pkg/patch/patch.go @@ -42,6 +42,29 @@ const ( defaultTag = "latest" ) +type BuildOpts struct { + BkClient *client.Client + SolveOpt *client.SolveOpt + Image string + Ch chan error + ReportFile string + WorkingFolder string + Updates *unversioned.UpdateManifest + IgnoreError bool + Output string + DockerNormalizedImageName reference.Named + PatchedImageName string + Format string +} + +type BuildStatus struct { + BuildChannel chan *client.SolveStatus +} + +type BuildContext struct { + Ctx context.Context +} + // 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, scanner, format, output string, ignoreError bool, bkOpts buildkit.Opts) error { timeoutCtx, cancel := context.WithTimeout(ctx, timeout) @@ -157,7 +180,14 @@ func patchWithContext(ctx context.Context, ch chan error, image, reportFile, use buildChannel := make(chan *client.SolveStatus) eg, ctx := errgroup.WithContext(ctx) eg.Go(func() error { - err = buildkitBuild(bkClient, ctx, &solveOpt, dockerNormalizedImageName, ch, reportFile, workingFolder, updates, ignoreError, output, patchedImageName, format, buildChannel) + err = buildkitBuild( + BuildContext{ctx}, + &BuildOpts{ + bkClient, &solveOpt, image, ch, + reportFile, workingFolder, updates, ignoreError, + output, dockerNormalizedImageName, patchedImageName, format, + }, + BuildStatus{buildChannel}, updates) return err }) @@ -186,49 +216,49 @@ func patchWithContext(ctx context.Context, ch chan error, image, reportFile, use return eg.Wait() } -func buildkitBuild(bkClient *client.Client, ctx context.Context, solveOpt *client.SolveOpt, dockerNormalizedImageName reference.Named, ch chan error, reportFile string, workingFolder string, updates *unversioned.UpdateManifest, ignoreError bool, output string, patchedImageName string, format string, buildChannel chan *client.SolveStatus) error { - _, err := bkClient.Build(ctx, *solveOpt, copaProduct, func(ctx context.Context, c gwclient.Client) (*gwclient.Result, error) { - bkConfig, err := buildkit.InitializeBuildkitConfig(ctx, c, dockerNormalizedImageName.String()) +func buildkitBuild(buildContext BuildContext, buildOpts *BuildOpts, buildStatus BuildStatus, updates *unversioned.UpdateManifest) error { + _, err := buildOpts.BkClient.Build(buildContext.Ctx, *buildOpts.SolveOpt, copaProduct, func(ctx context.Context, c gwclient.Client) (*gwclient.Result, error) { + bkConfig, err := buildkit.InitializeBuildkitConfig(ctx, c, buildOpts.DockerNormalizedImageName.String()) if err != nil { - return handleError(ch, err) + return handleError(buildOpts.Ch, err) } - manager, err := resolvePackageManager(ctx, c, bkConfig, reportFile, updates, workingFolder) + manager, err := resolvePackageManager(buildContext, buildOpts, c, bkConfig, updates) if err != nil { - return handleError(ch, err) + return handleError(buildOpts.Ch, err) } - return buildReport(ctx, ch, bkConfig, manager, updates, ignoreError, patchedImageName, format, output, reportFile) - }, buildChannel) + return buildReport(ctx, buildOpts.Ch, bkConfig, manager, buildOpts.Updates, buildOpts.IgnoreError, buildOpts.PatchedImageName, buildOpts.Format, buildOpts.Output, buildOpts.ReportFile) + }, buildStatus.BuildChannel) return err } -func resolvePackageManager(ctx context.Context, c gwclient.Client, config *buildkit.Config, reportFile string, updates *unversioned.UpdateManifest, workingFolder string) (pkgmgr.PackageManager, error) { +func resolvePackageManager(buildContext BuildContext, buildOpts *BuildOpts, client gwclient.Client, config *buildkit.Config, updates *unversioned.UpdateManifest) (pkgmgr.PackageManager, error) { var manager pkgmgr.PackageManager - if reportFile == "" { - fileBytes, err := buildkit.ExtractFileFromState(ctx, c, &config.ImageState, "/etc/os-release") + if buildOpts.ReportFile == "" { + fileBytes, err := buildkit.ExtractFileFromState(buildContext.Ctx, client, &config.ImageState, "/etc/os-release") if err != nil { return nil, err } - osType, err := getOSType(ctx, fileBytes) + osType, err := getOSType(buildContext.Ctx, fileBytes) if err != nil { return nil, err } - osVersion, err := getOSVersion(ctx, fileBytes) + osVersion, err := getOSVersion(buildContext.Ctx, fileBytes) if err != nil { return nil, err } // get package manager based on os family type - manager, err = pkgmgr.GetPackageManager(osType, osVersion, config, workingFolder) + manager, err = pkgmgr.GetPackageManager(osType, osVersion, config, buildOpts.WorkingFolder) if err != nil { return nil, err } } else { // get package manager based on os family type var err error - manager, err = pkgmgr.GetPackageManager(updates.Metadata.OS.Type, updates.Metadata.OS.Version, config, workingFolder) + manager, err = pkgmgr.GetPackageManager(updates.Metadata.OS.Type, updates.Metadata.OS.Version, config, buildOpts.WorkingFolder) if err != nil { return nil, err } From 338976cd0581c7de5d7df0276df847839d359f34 Mon Sep 17 00:00:00 2001 From: Miaha Cybersec Date: Wed, 31 Jul 2024 10:15:45 -0600 Subject: [PATCH 04/10] Refactor build functions for improved readability Signed-off-by: Miaha Cybersec --- pkg/patch/patch.go | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/pkg/patch/patch.go b/pkg/patch/patch.go index a952a7c45..616a1fc63 100644 --- a/pkg/patch/patch.go +++ b/pkg/patch/patch.go @@ -187,7 +187,7 @@ func patchWithContext(ctx context.Context, ch chan error, image, reportFile, use reportFile, workingFolder, updates, ignoreError, output, dockerNormalizedImageName, patchedImageName, format, }, - BuildStatus{buildChannel}, updates) + BuildStatus{buildChannel}) return err }) @@ -216,24 +216,24 @@ func patchWithContext(ctx context.Context, ch chan error, image, reportFile, use return eg.Wait() } -func buildkitBuild(buildContext BuildContext, buildOpts *BuildOpts, buildStatus BuildStatus, updates *unversioned.UpdateManifest) error { +func buildkitBuild(buildContext BuildContext, buildOpts *BuildOpts, buildStatus BuildStatus) error { _, err := buildOpts.BkClient.Build(buildContext.Ctx, *buildOpts.SolveOpt, copaProduct, func(ctx context.Context, c gwclient.Client) (*gwclient.Result, error) { bkConfig, err := buildkit.InitializeBuildkitConfig(ctx, c, buildOpts.DockerNormalizedImageName.String()) if err != nil { return handleError(buildOpts.Ch, err) } - manager, err := resolvePackageManager(buildContext, buildOpts, c, bkConfig, updates) + manager, err := resolvePackageManager(buildContext, buildOpts, c, bkConfig) if err != nil { return handleError(buildOpts.Ch, err) } - return buildReport(ctx, buildOpts.Ch, bkConfig, manager, buildOpts.Updates, buildOpts.IgnoreError, buildOpts.PatchedImageName, buildOpts.Format, buildOpts.Output, buildOpts.ReportFile) + return buildReport(buildContext, buildOpts, bkConfig, manager) }, buildStatus.BuildChannel) return err } -func resolvePackageManager(buildContext BuildContext, buildOpts *BuildOpts, client gwclient.Client, config *buildkit.Config, updates *unversioned.UpdateManifest) (pkgmgr.PackageManager, error) { +func resolvePackageManager(buildContext BuildContext, buildOpts *BuildOpts, client gwclient.Client, config *buildkit.Config) (pkgmgr.PackageManager, error) { var manager pkgmgr.PackageManager if buildOpts.ReportFile == "" { fileBytes, err := buildkit.ExtractFileFromState(buildContext.Ctx, client, &config.ImageState, "/etc/os-release") @@ -258,7 +258,7 @@ func resolvePackageManager(buildContext BuildContext, buildOpts *BuildOpts, clie } else { // get package manager based on os family type var err error - manager, err = pkgmgr.GetPackageManager(updates.Metadata.OS.Type, updates.Metadata.OS.Version, config, buildOpts.WorkingFolder) + manager, err = pkgmgr.GetPackageManager(buildOpts.Updates.Metadata.OS.Type, buildOpts.Updates.Metadata.OS.Version, config, buildOpts.WorkingFolder) if err != nil { return nil, err } @@ -273,35 +273,35 @@ func handleError(ch chan error, err error) (*gwclient.Result, error) { } // buildReport is an extracted method containing logic to manage the updates and build report. -func buildReport(ctx context.Context, ch chan error, config *buildkit.Config, manager pkgmgr.PackageManager, updates *unversioned.UpdateManifest, ignoreError bool, patchedImageName string, format string, output string, reportFile string) (*gwclient.Result, error) { - patchedImageState, errPkgs, err := manager.InstallUpdates(ctx, updates, ignoreError) +func buildReport(buildContext BuildContext, buildOpts *BuildOpts, config *buildkit.Config, manager pkgmgr.PackageManager) (*gwclient.Result, error) { + patchedImageState, errPkgs, err := manager.InstallUpdates(buildContext.Ctx, buildOpts.Updates, buildOpts.IgnoreError) if err != nil { - return handleError(ch, err) + return handleError(buildOpts.Ch, err) } platform := platforms.Normalize(platforms.DefaultSpec()) if platform.OS != "linux" { platform.OS = "linux" } - def, err := patchedImageState.Marshal(ctx, llb.Platform(platform)) + def, err := patchedImageState.Marshal(buildContext.Ctx, llb.Platform(platform)) if err != nil { - return handleError(ch, fmt.Errorf("unable to get platform from ImageState %w", err)) + return handleError(buildOpts.Ch, fmt.Errorf("unable to get platform from ImageState %w", err)) } - res, err := config.Client.Solve(ctx, gwclient.SolveRequest{ + res, err := config.Client.Solve(buildContext.Ctx, gwclient.SolveRequest{ Definition: def.ToPB(), Evaluate: true, }) if err != nil { - return handleError(ch, err) + return handleError(buildOpts.Ch, err) } res.AddMeta(exptypes.ExporterImageConfigKey, config.ConfigData) // Currently can only validate updates if updating via scanner - if reportFile != "" { - validatedManifest := updateManifest(updates, errPkgs) + if buildOpts.ReportFile != "" { + validatedManifest := updateManifest(buildOpts.Updates, errPkgs) // vex document must contain at least one statement - if output != "" && len(validatedManifest.Updates) > 0 { - err = vex.TryOutputVexDocument(validatedManifest, manager, patchedImageName, format, output) + if buildOpts.Output != "" && len(validatedManifest.Updates) > 0 { + err = vex.TryOutputVexDocument(validatedManifest, manager, buildOpts.PatchedImageName, buildOpts.Format, buildOpts.Output) if err != nil { - return handleError(ch, err) + return handleError(buildOpts.Ch, err) } } } From 0a4fb60fc12a5a75614a4d95075f5397cd44a79e Mon Sep 17 00:00:00 2001 From: Miaha Cybersec Date: Wed, 31 Jul 2024 13:14:21 -0600 Subject: [PATCH 05/10] Refactor dockerLoad error handling and update tag logic Signed-off-by: Miaha Cybersec --- pkg/patch/patch.go | 5 +- pkg/patch/patch_test.go | 105 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 106 insertions(+), 4 deletions(-) diff --git a/pkg/patch/patch.go b/pkg/patch/patch.go index 616a1fc63..f8e9c7300 100644 --- a/pkg/patch/patch.go +++ b/pkg/patch/patch.go @@ -207,7 +207,8 @@ func patchWithContext(ctx context.Context, ch chan error, image, reportFile, use }) eg.Go(func() error { - if err := dockerLoad(ctx, pipeR); err != nil { + err = dockerLoad(ctx, pipeR) + if err != nil { return err } return pipeR.Close() @@ -344,7 +345,7 @@ func generatePatchedTag(dockerNormalizedImageName reference.Named, userSuppliedP } if userSuppliedPatchTag != "" { - copaTag = fmt.Sprintf("%s-%s", officialTag, userSuppliedPatchTag) + copaTag = userSuppliedPatchTag return copaTag } else if officialTag == "" { log.Warnf("No output tag specified for digest-referenced image, defaulting to `%s`", defaultPatchedTagSuffix) diff --git a/pkg/patch/patch_test.go b/pkg/patch/patch_test.go index d0ff16a88..71d65c65e 100644 --- a/pkg/patch/patch_test.go +++ b/pkg/patch/patch_test.go @@ -4,8 +4,11 @@ import ( "context" "errors" "os" + "reflect" "testing" + "github.com/project-copacetic/copacetic/pkg/types/unversioned" + "github.com/distribution/reference" log "github.com/sirupsen/logrus" @@ -316,8 +319,8 @@ func TestGeneratePatchedTag(t *testing.T) { { name: "WithTag_UserSupplied", dockerImageName: "docker.io/librari/ubuntu:jammy-20231004", - userSuppliedPatchTag: "01", - expectedPatchedTag: "jammy-20231004-01", + userSuppliedPatchTag: "20231004-patched", + expectedPatchedTag: "20231004-patched", }, } @@ -331,3 +334,101 @@ func TestGeneratePatchedTag(t *testing.T) { }) } } + +func TestUpdateManifest(t *testing.T) { + errPkgs := []string{"package1", "package2"} + + updates := &unversioned.UpdateManifest{ + Metadata: unversioned.Metadata{ + OS: unversioned.OS{ + Type: "Linux", + Version: "5.0.1", + }, + Config: unversioned.Config{ + Arch: "x86_64", + }, + }, + Updates: []unversioned.UpdatePackage{ + {Name: "package1"}, + {Name: "package2"}, + {Name: "package3"}, + }, + } + + testCases := []struct { + name string + updates *unversioned.UpdateManifest + errPkgs []string + expected *unversioned.UpdateManifest + }{ + { + name: "NoErrorPackages", + updates: updates, + errPkgs: []string{}, + expected: &unversioned.UpdateManifest{ + Metadata: unversioned.Metadata{ + OS: unversioned.OS{ + Type: "Linux", + Version: "5.0.1", + }, + Config: unversioned.Config{ + Arch: "x86_64", + }, + }, + Updates: []unversioned.UpdatePackage{ + {Name: "package1"}, + {Name: "package2"}, + {Name: "package3"}, + }, + }, + }, + { + name: "AllErrorPackages", + updates: updates, + errPkgs: errPkgs, + expected: &unversioned.UpdateManifest{ + Metadata: unversioned.Metadata{ + OS: unversioned.OS{ + Type: "Linux", + Version: "5.0.1", + }, + Config: unversioned.Config{ + Arch: "x86_64", + }, + }, + Updates: []unversioned.UpdatePackage{ + {Name: "package3"}, + }, + }, + }, + { + name: "SomeErrorPackages", + updates: updates, + errPkgs: []string{"package1"}, + expected: &unversioned.UpdateManifest{ + Metadata: unversioned.Metadata{ + OS: unversioned.OS{ + Type: "Linux", + Version: "5.0.1", + }, + Config: unversioned.Config{ + Arch: "x86_64", + }, + }, + Updates: []unversioned.UpdatePackage{ + {Name: "package2"}, + {Name: "package3"}, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual := updateManifest(tc.updates, tc.errPkgs) + if !reflect.DeepEqual(actual, tc.expected) { + t.Errorf("TestUpdateManifest(%v, %v): expected %v, actual %v", tc.updates, tc.errPkgs, tc.expected, actual) + } + }) + } +} From 8a12e7a2f5c80d0d0093d9730a5ba81ff460794e Mon Sep 17 00:00:00 2001 From: Miaha Cybersec Date: Wed, 31 Jul 2024 16:04:22 -0600 Subject: [PATCH 06/10] Add unit tests for error handling and Docker load Signed-off-by: Miaha Cybersec --- pkg/patch/patch_test.go | 99 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 95 insertions(+), 4 deletions(-) diff --git a/pkg/patch/patch_test.go b/pkg/patch/patch_test.go index 71d65c65e..872150a7a 100644 --- a/pkg/patch/patch_test.go +++ b/pkg/patch/patch_test.go @@ -3,7 +3,9 @@ package patch import ( "context" "errors" + "io" "os" + "os/exec" "reflect" "testing" @@ -336,7 +338,7 @@ func TestGeneratePatchedTag(t *testing.T) { } func TestUpdateManifest(t *testing.T) { - errPkgs := []string{"package1", "package2"} + errPkgs := []string{"package1", "package2", "package3"} updates := &unversioned.UpdateManifest{ Metadata: unversioned.Metadata{ @@ -396,9 +398,7 @@ func TestUpdateManifest(t *testing.T) { Arch: "x86_64", }, }, - Updates: []unversioned.UpdatePackage{ - {Name: "package3"}, - }, + Updates: []unversioned.UpdatePackage{}, }, }, { @@ -432,3 +432,94 @@ func TestUpdateManifest(t *testing.T) { }) } } + +func TestHandleError(t *testing.T) { + tests := []struct { + name string + err error + wantErr bool + }{ + { + name: "no error", + err: nil, + wantErr: false, + }, + { + name: "test error", + err: errors.New("test error"), + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ch := make(chan error, 1) + defer close(ch) + + _, err := handleError(ch, tt.err) + + select { + case chErr := <-ch: + if (chErr == nil && tt.wantErr) || (chErr != nil && !tt.wantErr) { + t.Errorf("Error channel did not return expected error, got: %v, want: %v", chErr, tt.err) + } + default: + if tt.wantErr { + t.Error("Expected handleError to send error to error channel but it did not") + } + } + + if (err == nil && tt.wantErr) || (err != nil && !tt.wantErr) { + t.Errorf("handleError() error = %v, wantErr = %v", err, tt.wantErr) + } + }) + } +} + +// define a mock reader +type mockReader struct { + data []byte + err error +} + +func (mr *mockReader) Read(p []byte) (int, error) { + copy(p, mr.data) + return len(mr.data), mr.err +} + +func TestDockerLoad(t *testing.T) { + ctx := context.TODO() + + testCases := []struct { + name string + pipeR io.Reader + mockCmd *exec.Cmd + expectErr bool + }{ + { + name: "Unrecognized image format", + pipeR: &mockReader{nil, errors.New("unrecognized image format")}, + mockCmd: exec.Command("echo", "test"), + expectErr: true, + }, + { + name: "Invalid tar header", + pipeR: &mockReader{[]byte("alpine:latest"), errors.New("unrecognized tar header")}, + // this command is likely to fail which is desired for this test case + mockCmd: exec.Command("docker", "load"), + expectErr: true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + err := dockerLoad(ctx, testCase.pipeR) + if testCase.expectErr && err == nil { + t.Errorf("expected an error but got none") + } + if !testCase.expectErr && err != nil { + t.Errorf("did not expect an error but got %v", err) + } + }) + } +} From 42ccc738f2408fea9d227e71eebafc9296ea6a7f Mon Sep 17 00:00:00 2001 From: Miaha Cybersec Date: Mon, 5 Aug 2024 10:56:33 -0600 Subject: [PATCH 07/10] Rename BuildOpts to TrivyOpts Signed-off-by: Miaha Cybersec --- pkg/patch/patch.go | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/pkg/patch/patch.go b/pkg/patch/patch.go index f8e9c7300..6f3367e19 100644 --- a/pkg/patch/patch.go +++ b/pkg/patch/patch.go @@ -42,7 +42,7 @@ const ( defaultTag = "latest" ) -type BuildOpts struct { +type TrivyOpts struct { BkClient *client.Client SolveOpt *client.SolveOpt Image string @@ -182,7 +182,7 @@ func patchWithContext(ctx context.Context, ch chan error, image, reportFile, use eg.Go(func() error { err = buildkitBuild( BuildContext{ctx}, - &BuildOpts{ + &TrivyOpts{ bkClient, &solveOpt, image, ch, reportFile, workingFolder, updates, ignoreError, output, dockerNormalizedImageName, patchedImageName, format, @@ -217,26 +217,26 @@ func patchWithContext(ctx context.Context, ch chan error, image, reportFile, use return eg.Wait() } -func buildkitBuild(buildContext BuildContext, buildOpts *BuildOpts, buildStatus BuildStatus) error { - _, err := buildOpts.BkClient.Build(buildContext.Ctx, *buildOpts.SolveOpt, copaProduct, func(ctx context.Context, c gwclient.Client) (*gwclient.Result, error) { - bkConfig, err := buildkit.InitializeBuildkitConfig(ctx, c, buildOpts.DockerNormalizedImageName.String()) +func buildkitBuild(buildContext BuildContext, trivyOpts *TrivyOpts, buildStatus BuildStatus) error { + _, err := trivyOpts.BkClient.Build(buildContext.Ctx, *trivyOpts.SolveOpt, copaProduct, func(ctx context.Context, c gwclient.Client) (*gwclient.Result, error) { + bkConfig, err := buildkit.InitializeBuildkitConfig(ctx, c, trivyOpts.DockerNormalizedImageName.String()) if err != nil { - return handleError(buildOpts.Ch, err) + return handleError(trivyOpts.Ch, err) } - manager, err := resolvePackageManager(buildContext, buildOpts, c, bkConfig) + manager, err := resolvePackageManager(buildContext, trivyOpts, c, bkConfig) if err != nil { - return handleError(buildOpts.Ch, err) + return handleError(trivyOpts.Ch, err) } - return buildReport(buildContext, buildOpts, bkConfig, manager) + return buildReport(buildContext, trivyOpts, bkConfig, manager) }, buildStatus.BuildChannel) return err } -func resolvePackageManager(buildContext BuildContext, buildOpts *BuildOpts, client gwclient.Client, config *buildkit.Config) (pkgmgr.PackageManager, error) { +func resolvePackageManager(buildContext BuildContext, trivyOpts *TrivyOpts, client gwclient.Client, config *buildkit.Config) (pkgmgr.PackageManager, error) { var manager pkgmgr.PackageManager - if buildOpts.ReportFile == "" { + if trivyOpts.ReportFile == "" { fileBytes, err := buildkit.ExtractFileFromState(buildContext.Ctx, client, &config.ImageState, "/etc/os-release") if err != nil { return nil, err @@ -252,14 +252,14 @@ func resolvePackageManager(buildContext BuildContext, buildOpts *BuildOpts, clie return nil, err } // get package manager based on os family type - manager, err = pkgmgr.GetPackageManager(osType, osVersion, config, buildOpts.WorkingFolder) + manager, err = pkgmgr.GetPackageManager(osType, osVersion, config, trivyOpts.WorkingFolder) if err != nil { return nil, err } } else { // get package manager based on os family type var err error - manager, err = pkgmgr.GetPackageManager(buildOpts.Updates.Metadata.OS.Type, buildOpts.Updates.Metadata.OS.Version, config, buildOpts.WorkingFolder) + manager, err = pkgmgr.GetPackageManager(trivyOpts.Updates.Metadata.OS.Type, trivyOpts.Updates.Metadata.OS.Version, config, trivyOpts.WorkingFolder) if err != nil { return nil, err } @@ -274,10 +274,10 @@ func handleError(ch chan error, err error) (*gwclient.Result, error) { } // buildReport is an extracted method containing logic to manage the updates and build report. -func buildReport(buildContext BuildContext, buildOpts *BuildOpts, config *buildkit.Config, manager pkgmgr.PackageManager) (*gwclient.Result, error) { - patchedImageState, errPkgs, err := manager.InstallUpdates(buildContext.Ctx, buildOpts.Updates, buildOpts.IgnoreError) +func buildReport(buildContext BuildContext, trivyOpts *TrivyOpts, config *buildkit.Config, manager pkgmgr.PackageManager) (*gwclient.Result, error) { + patchedImageState, errPkgs, err := manager.InstallUpdates(buildContext.Ctx, trivyOpts.Updates, trivyOpts.IgnoreError) if err != nil { - return handleError(buildOpts.Ch, err) + return handleError(trivyOpts.Ch, err) } platform := platforms.Normalize(platforms.DefaultSpec()) if platform.OS != "linux" { @@ -285,24 +285,24 @@ func buildReport(buildContext BuildContext, buildOpts *BuildOpts, config *buildk } def, err := patchedImageState.Marshal(buildContext.Ctx, llb.Platform(platform)) if err != nil { - return handleError(buildOpts.Ch, fmt.Errorf("unable to get platform from ImageState %w", err)) + return handleError(trivyOpts.Ch, fmt.Errorf("unable to get platform from ImageState %w", err)) } res, err := config.Client.Solve(buildContext.Ctx, gwclient.SolveRequest{ Definition: def.ToPB(), Evaluate: true, }) if err != nil { - return handleError(buildOpts.Ch, err) + return handleError(trivyOpts.Ch, err) } res.AddMeta(exptypes.ExporterImageConfigKey, config.ConfigData) // Currently can only validate updates if updating via scanner - if buildOpts.ReportFile != "" { - validatedManifest := updateManifest(buildOpts.Updates, errPkgs) + if trivyOpts.ReportFile != "" { + validatedManifest := updateManifest(trivyOpts.Updates, errPkgs) // vex document must contain at least one statement - if buildOpts.Output != "" && len(validatedManifest.Updates) > 0 { - err = vex.TryOutputVexDocument(validatedManifest, manager, buildOpts.PatchedImageName, buildOpts.Format, buildOpts.Output) + if trivyOpts.Output != "" && len(validatedManifest.Updates) > 0 { + err = vex.TryOutputVexDocument(validatedManifest, manager, trivyOpts.PatchedImageName, trivyOpts.Format, trivyOpts.Output) if err != nil { - return handleError(buildOpts.Ch, err) + return handleError(trivyOpts.Ch, err) } } } From 1b9c7e780d4308388d507f8d69db61dc687ba8ac Mon Sep 17 00:00:00 2001 From: Miaha Cybersec Date: Mon, 5 Aug 2024 12:16:42 -0600 Subject: [PATCH 08/10] golangci lint Signed-off-by: Miaha Cybersec --- pkg/patch/patch_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/patch/patch_test.go b/pkg/patch/patch_test.go index 872150a7a..257dcb3a9 100644 --- a/pkg/patch/patch_test.go +++ b/pkg/patch/patch_test.go @@ -476,7 +476,7 @@ func TestHandleError(t *testing.T) { } } -// define a mock reader +// define a mock reader. type mockReader struct { data []byte err error From 3941bccf1cb19db50627c27176be4f92971044ea Mon Sep 17 00:00:00 2001 From: Miaha Cybersec Date: Wed, 7 Aug 2024 09:55:52 -0600 Subject: [PATCH 09/10] Refactor TrivyOpts to separate BuildKit client options, add documentation Extracted BuildKit client configurations from TrivyOpts into a new BkClient struct. Updated related functions and tests to accommodate this change, improving modularity and readability. Signed-off-by: Miaha Cybersec --- pkg/patch/patch.go | 29 ++++++++++++++++++++++------- pkg/patch/patch_test.go | 21 +++++++++++++++------ 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/pkg/patch/patch.go b/pkg/patch/patch.go index 6f3367e19..7b6843806 100644 --- a/pkg/patch/patch.go +++ b/pkg/patch/patch.go @@ -43,8 +43,6 @@ const ( ) type TrivyOpts struct { - BkClient *client.Client - SolveOpt *client.SolveOpt Image string Ch chan error ReportFile string @@ -57,6 +55,11 @@ type TrivyOpts struct { Format string } +type BkClient struct { + BkClient *client.Client + SolveOpt *client.SolveOpt +} + type BuildStatus struct { BuildChannel chan *client.SolveStatus } @@ -97,7 +100,15 @@ func removeIfNotDebug(workingFolder string) { } } -// patchWithContext patches the user-supplied image, image. +// patchWithContext patches the user-supplied image, image +// reportFile is a vulnerability scan passed in by the user +// userSuppliedPatchTag is a tag set by the user to use for the patched image tag +// workingFolder is the folder used by copa, defaults to system temp folder +// scanner used to generate reportFile, defaults to Trivy +// format is the output format, defaults to openvex +// output is the desired output filepath +// ignoreError defines whether Copa should ignore errors +// bkOpts contains buildkitd options for addresses, CA certs, client certs, and client keys. func patchWithContext(ctx context.Context, ch chan error, image, reportFile, userSuppliedPatchTag, workingFolder, scanner, format, output string, ignoreError bool, bkOpts buildkit.Opts) error { dockerNormalizedImageName, err := reference.ParseNormalizedNamed(image) if err != nil { @@ -105,7 +116,7 @@ func patchWithContext(ctx context.Context, ch chan error, image, reportFile, use } if reference.IsNameOnly(dockerNormalizedImageName) { - log.Warnf("Image name has no tag or digest, using latest as tag") + log.Warnf("Image name %s has no tag or digest, defaulting to %s:latest", image, dockerNormalizedImageName) dockerNormalizedImageName = reference.TagNameOnly(dockerNormalizedImageName) } @@ -183,10 +194,13 @@ func patchWithContext(ctx context.Context, ch chan error, image, reportFile, use err = buildkitBuild( BuildContext{ctx}, &TrivyOpts{ - bkClient, &solveOpt, image, ch, + image, ch, reportFile, workingFolder, updates, ignoreError, output, dockerNormalizedImageName, patchedImageName, format, }, + BkClient{ + bkClient, &solveOpt, + }, BuildStatus{buildChannel}) return err }) @@ -217,8 +231,9 @@ func patchWithContext(ctx context.Context, ch chan error, image, reportFile, use return eg.Wait() } -func buildkitBuild(buildContext BuildContext, trivyOpts *TrivyOpts, buildStatus BuildStatus) error { - _, err := trivyOpts.BkClient.Build(buildContext.Ctx, *trivyOpts.SolveOpt, copaProduct, func(ctx context.Context, c gwclient.Client) (*gwclient.Result, error) { +// buildkitBuild submits a build request to BuildKit with the given information. +func buildkitBuild(buildContext BuildContext, trivyOpts *TrivyOpts, bkClient BkClient, buildStatus BuildStatus) error { + _, err := bkClient.BkClient.Build(buildContext.Ctx, *bkClient.SolveOpt, copaProduct, func(ctx context.Context, c gwclient.Client) (*gwclient.Result, error) { bkConfig, err := buildkit.InitializeBuildkitConfig(ctx, c, trivyOpts.DockerNormalizedImageName.String()) if err != nil { return handleError(trivyOpts.Ch, err) diff --git a/pkg/patch/patch_test.go b/pkg/patch/patch_test.go index 257dcb3a9..e75afbca9 100644 --- a/pkg/patch/patch_test.go +++ b/pkg/patch/patch_test.go @@ -3,6 +3,7 @@ package patch import ( "context" "errors" + "fmt" "io" "os" "os/exec" @@ -299,6 +300,8 @@ func TestGetOSVersion(t *testing.T) { } } +// Test generating a patched tag for an image +// If userSuppliedPatchTag is a blank string, the function defaults to defaultPatchedTagSuffix. func TestGeneratePatchedTag(t *testing.T) { testCases := []struct { name string @@ -312,17 +315,23 @@ func TestGeneratePatchedTag(t *testing.T) { userSuppliedPatchTag: "", expectedPatchedTag: defaultPatchedTagSuffix, }, + { + name: "NoTag_UserSupplied", + dockerImageName: "docker.io/library/alpine", + userSuppliedPatchTag: "1234", + expectedPatchedTag: "1234", + }, { name: "WithTag_NoUserSupplied", dockerImageName: "docker.io/redhat/ubi9:latest", userSuppliedPatchTag: "", - expectedPatchedTag: "latest-patched", + expectedPatchedTag: fmt.Sprintf("latest-%s", defaultPatchedTagSuffix), }, { name: "WithTag_UserSupplied", dockerImageName: "docker.io/librari/ubuntu:jammy-20231004", - userSuppliedPatchTag: "20231004-patched", - expectedPatchedTag: "20231004-patched", + userSuppliedPatchTag: "20231004-custom-tag", + expectedPatchedTag: "20231004-custom-tag", }, } @@ -337,9 +346,8 @@ func TestGeneratePatchedTag(t *testing.T) { } } +// This test simulates the vulnerable packages Trivy supplies to Copa. func TestUpdateManifest(t *testing.T) { - errPkgs := []string{"package1", "package2", "package3"} - updates := &unversioned.UpdateManifest{ Metadata: unversioned.Metadata{ OS: unversioned.OS{ @@ -357,6 +365,7 @@ func TestUpdateManifest(t *testing.T) { }, } + // errPkgs in this struct is used to define which packages would throw an error during the update process testCases := []struct { name string updates *unversioned.UpdateManifest @@ -387,7 +396,7 @@ func TestUpdateManifest(t *testing.T) { { name: "AllErrorPackages", updates: updates, - errPkgs: errPkgs, + errPkgs: []string{"package1", "package2", "package3"}, expected: &unversioned.UpdateManifest{ Metadata: unversioned.Metadata{ OS: unversioned.OS{ From 6f4a12a0a9e1a7e5dd109092cbb855feb4a86254 Mon Sep 17 00:00:00 2001 From: Miaha Cybersec Date: Wed, 14 Aug 2024 09:45:53 -0600 Subject: [PATCH 10/10] Rename `TrivyOpts` to `ScannerOpts` and refactor error handling Renamed the `TrivyOpts` struct to `ScannerOpts` to better reflect its purpose. Added an explicit channel parameter `ch` for error handling throughout the functions to improve code clarity and maintainability. Added new test cases to cover scenarios with digest-referenced Docker images. Signed-off-by: Miaha Cybersec --- pkg/patch/patch.go | 40 +++++++++++++++++++--------------------- pkg/patch/patch_test.go | 12 ++++++++++++ 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/pkg/patch/patch.go b/pkg/patch/patch.go index 7b6843806..aeb7a03bc 100644 --- a/pkg/patch/patch.go +++ b/pkg/patch/patch.go @@ -42,9 +42,8 @@ const ( defaultTag = "latest" ) -type TrivyOpts struct { +type ScannerOpts struct { Image string - Ch chan error ReportFile string WorkingFolder string Updates *unversioned.UpdateManifest @@ -193,15 +192,14 @@ func patchWithContext(ctx context.Context, ch chan error, image, reportFile, use eg.Go(func() error { err = buildkitBuild( BuildContext{ctx}, - &TrivyOpts{ - image, ch, - reportFile, workingFolder, updates, ignoreError, + &ScannerOpts{ + image, reportFile, workingFolder, updates, ignoreError, output, dockerNormalizedImageName, patchedImageName, format, }, BkClient{ bkClient, &solveOpt, }, - BuildStatus{buildChannel}) + BuildStatus{buildChannel}, ch) return err }) @@ -232,24 +230,24 @@ func patchWithContext(ctx context.Context, ch chan error, image, reportFile, use } // buildkitBuild submits a build request to BuildKit with the given information. -func buildkitBuild(buildContext BuildContext, trivyOpts *TrivyOpts, bkClient BkClient, buildStatus BuildStatus) error { +func buildkitBuild(buildContext BuildContext, trivyOpts *ScannerOpts, bkClient BkClient, buildStatus BuildStatus, ch chan error) error { _, err := bkClient.BkClient.Build(buildContext.Ctx, *bkClient.SolveOpt, copaProduct, func(ctx context.Context, c gwclient.Client) (*gwclient.Result, error) { bkConfig, err := buildkit.InitializeBuildkitConfig(ctx, c, trivyOpts.DockerNormalizedImageName.String()) if err != nil { - return handleError(trivyOpts.Ch, err) + return handleError(ch, err) } manager, err := resolvePackageManager(buildContext, trivyOpts, c, bkConfig) if err != nil { - return handleError(trivyOpts.Ch, err) + return handleError(ch, err) } - return buildReport(buildContext, trivyOpts, bkConfig, manager) + return buildReport(buildContext, trivyOpts, bkConfig, manager, ch) }, buildStatus.BuildChannel) return err } -func resolvePackageManager(buildContext BuildContext, trivyOpts *TrivyOpts, client gwclient.Client, config *buildkit.Config) (pkgmgr.PackageManager, error) { +func resolvePackageManager(buildContext BuildContext, trivyOpts *ScannerOpts, client gwclient.Client, config *buildkit.Config) (pkgmgr.PackageManager, error) { var manager pkgmgr.PackageManager if trivyOpts.ReportFile == "" { fileBytes, err := buildkit.ExtractFileFromState(buildContext.Ctx, client, &config.ImageState, "/etc/os-release") @@ -289,10 +287,10 @@ func handleError(ch chan error, err error) (*gwclient.Result, error) { } // buildReport is an extracted method containing logic to manage the updates and build report. -func buildReport(buildContext BuildContext, trivyOpts *TrivyOpts, config *buildkit.Config, manager pkgmgr.PackageManager) (*gwclient.Result, error) { +func buildReport(buildContext BuildContext, trivyOpts *ScannerOpts, config *buildkit.Config, manager pkgmgr.PackageManager, ch chan error) (*gwclient.Result, error) { patchedImageState, errPkgs, err := manager.InstallUpdates(buildContext.Ctx, trivyOpts.Updates, trivyOpts.IgnoreError) if err != nil { - return handleError(trivyOpts.Ch, err) + return handleError(ch, err) } platform := platforms.Normalize(platforms.DefaultSpec()) if platform.OS != "linux" { @@ -300,14 +298,14 @@ func buildReport(buildContext BuildContext, trivyOpts *TrivyOpts, config *buildk } def, err := patchedImageState.Marshal(buildContext.Ctx, llb.Platform(platform)) if err != nil { - return handleError(trivyOpts.Ch, fmt.Errorf("unable to get platform from ImageState %w", err)) + return handleError(ch, fmt.Errorf("unable to get platform from ImageState %w", err)) } res, err := config.Client.Solve(buildContext.Ctx, gwclient.SolveRequest{ Definition: def.ToPB(), Evaluate: true, }) if err != nil { - return handleError(trivyOpts.Ch, err) + return handleError(ch, err) } res.AddMeta(exptypes.ExporterImageConfigKey, config.ConfigData) // Currently can only validate updates if updating via scanner @@ -317,7 +315,7 @@ func buildReport(buildContext BuildContext, trivyOpts *TrivyOpts, config *buildk if trivyOpts.Output != "" && len(validatedManifest.Updates) > 0 { err = vex.TryOutputVexDocument(validatedManifest, manager, trivyOpts.PatchedImageName, trivyOpts.Format, trivyOpts.Output) if err != nil { - return handleError(trivyOpts.Ch, err) + return handleError(ch, err) } } } @@ -347,14 +345,14 @@ func updateManifest(updates *unversioned.UpdateManifest, errPkgs []string) *unve } func generatePatchedTag(dockerNormalizedImageName reference.Named, userSuppliedPatchTag string) string { - // officialTag is typically the versioning tag of the image as published in a container registry - var officialTag string + // currentTag is typically the versioning tag of the image as published in a container registry + var currentTag string var copaTag string taggedName, ok := dockerNormalizedImageName.(reference.Tagged) if ok { - officialTag = taggedName.Tag() + currentTag = taggedName.Tag() } else { log.Warnf("Image name has no tag") } @@ -362,13 +360,13 @@ func generatePatchedTag(dockerNormalizedImageName reference.Named, userSuppliedP if userSuppliedPatchTag != "" { copaTag = userSuppliedPatchTag return copaTag - } else if officialTag == "" { + } else if currentTag == "" { log.Warnf("No output tag specified for digest-referenced image, defaulting to `%s`", defaultPatchedTagSuffix) copaTag = defaultPatchedTagSuffix return copaTag } - copaTag = fmt.Sprintf("%s-%s", officialTag, defaultPatchedTagSuffix) + copaTag = fmt.Sprintf("%s-%s", currentTag, defaultPatchedTagSuffix) return copaTag } diff --git a/pkg/patch/patch_test.go b/pkg/patch/patch_test.go index e75afbca9..4d4f8d7af 100644 --- a/pkg/patch/patch_test.go +++ b/pkg/patch/patch_test.go @@ -333,6 +333,18 @@ func TestGeneratePatchedTag(t *testing.T) { userSuppliedPatchTag: "20231004-custom-tag", expectedPatchedTag: "20231004-custom-tag", }, + { + name: "NoTag_WithDigest_NoUserSupplied", + dockerImageName: "docker.io/library/debian@sha256:540ebf19fb0bbc243e1314edac26b9fe7445e9c203357f27968711a45ea9f1d4", + userSuppliedPatchTag: "", + expectedPatchedTag: defaultPatchedTagSuffix, + }, + { + name: "NoTag_WithDigest_UserSupplied", + dockerImageName: "docker.io/library/debian@sha256:540ebf19fb0bbc243e1314edac26b9fe7445e9c203357f27968711a45ea9f1d4", + userSuppliedPatchTag: "stable-patched", + expectedPatchedTag: "stable-patched", + }, } for _, tc := range testCases {