diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 5b64f42c6..17825bc1d 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -26,3 +26,6 @@ If applicable, add screenshots or logs to help explain your problem.
 
 **Additional context**
 Add any other context about the problem here.
+
+
+_To help debug the issue as quickly as possible, we recommend generating a support bundle with `finch support-bundle-generate` and attaching it to this issue. This packages all Finch-related configs and logs into one file._
\ No newline at end of file
diff --git a/cmd/finch/main.go b/cmd/finch/main.go
index 13ed92f23..799c0ec96 100644
--- a/cmd/finch/main.go
+++ b/cmd/finch/main.go
@@ -9,16 +9,16 @@ import (
 	"io"
 	"os"
 
-	"github.com/runfinch/finch/pkg/disk"
-
 	"github.com/runfinch/finch/pkg/command"
 	"github.com/runfinch/finch/pkg/config"
 	"github.com/runfinch/finch/pkg/dependency"
 	"github.com/runfinch/finch/pkg/dependency/vmnet"
+	"github.com/runfinch/finch/pkg/disk"
 	"github.com/runfinch/finch/pkg/flog"
 	"github.com/runfinch/finch/pkg/fmemory"
 	"github.com/runfinch/finch/pkg/fssh"
 	"github.com/runfinch/finch/pkg/path"
+	"github.com/runfinch/finch/pkg/support"
 	"github.com/runfinch/finch/pkg/system"
 	"github.com/runfinch/finch/pkg/version"
 
@@ -88,6 +88,13 @@ var newApp = func(logger flog.Logger, fp path.Finch, fs afero.Fs, fc *config.Fin
 		fp.QEMUBinDir(),
 		system.NewStdLib(),
 	)
+	supportBundleBuilder := support.NewBundleBuilder(
+		logger,
+		fs,
+		support.NewBundleConfig(fp, system.NewStdLib().Env("HOME")),
+		fp,
+		ecc,
+	)
 
 	// append nerdctl commands
 	allCommands := initializeNerdctlCommands(lcc, logger, fs)
@@ -95,6 +102,7 @@ var newApp = func(logger flog.Logger, fp path.Finch, fs afero.Fs, fc *config.Fin
 	allCommands = append(allCommands,
 		newVersionCommand(lcc, logger, stdOut),
 		virtualMachineCommands(logger, fp, lcc, ecc, fs, fc),
+		newSupportBundleCommand(logger, supportBundleBuilder, lcc),
 	)
 
 	rootCmd.AddCommand(allCommands...)
diff --git a/cmd/finch/main_test.go b/cmd/finch/main_test.go
index 09a16f50a..43df113a7 100644
--- a/cmd/finch/main_test.go
+++ b/cmd/finch/main_test.go
@@ -138,8 +138,8 @@ func TestNewApp(t *testing.T) {
 	assert.Equal(t, cmd.Version, version.Version)
 	assert.Equal(t, cmd.SilenceUsage, true)
 	assert.Equal(t, cmd.SilenceErrors, true)
-	// confirm the number of command, comprised of nerdctl commands + finch commands (version, vm)
-	assert.Equal(t, len(cmd.Commands()), len(nerdctlCmds)+2)
+	// confirm the number of command, comprised of nerdctl commands + finch commands (version, vm, support-bundle)
+	assert.Equal(t, len(cmd.Commands()), len(nerdctlCmds)+3)
 
 	// PersistentPreRunE should set logger level to debug if the debug flag exists.
 	mockCmd := &cobra.Command{}
diff --git a/cmd/finch/support_bundle.go b/cmd/finch/support_bundle.go
new file mode 100644
index 000000000..ffabc1c0f
--- /dev/null
+++ b/cmd/finch/support_bundle.go
@@ -0,0 +1,104 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package main
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+
+	"github.com/runfinch/finch/pkg/command"
+	"github.com/runfinch/finch/pkg/flog"
+	"github.com/runfinch/finch/pkg/lima"
+	"github.com/runfinch/finch/pkg/support"
+)
+
+func newSupportBundleCommand(logger flog.Logger, builder support.BundleBuilder, lcc command.LimaCmdCreator) *cobra.Command {
+	supportBundleCommand := &cobra.Command{
+		Use:   "support-bundle",
+		Short: "Support bundle management",
+	}
+	supportBundleCommand.AddCommand(
+		newSupportBundleGenerateCommand(logger, builder, lcc),
+	)
+	return supportBundleCommand
+}
+
+func newSupportBundleGenerateCommand(logger flog.Logger, builder support.BundleBuilder, lcc command.LimaCmdCreator) *cobra.Command {
+	supportBundleGenerateCommand := &cobra.Command{
+		Use:   "generate",
+		Args:  cobra.NoArgs,
+		Short: "Generate support bundle",
+		Long:  "Generates a collection of logs and configs that can be uploaded to a Github issue to help debug issues.",
+		RunE:  newGenerateSupportBundleAction(logger, builder, lcc).runAdapter,
+	}
+
+	supportBundleGenerateCommand.Flags().StringArray("include", []string{},
+		"additional files to include in the support bundle, specified by absolute or relative path")
+	supportBundleGenerateCommand.Flags().StringArray("exclude", []string{},
+		//nolint:lll // usage string
+		"files to exclude from the support bundle. if you specify a base name, all files matching that base name will be excluded. if you specify an absolute or relative path, only exact matches will be excluded")
+	return supportBundleGenerateCommand
+}
+
+type generateSupportBundleAction struct {
+	logger  flog.Logger
+	builder support.BundleBuilder
+	lcc     command.LimaCmdCreator
+}
+
+func newGenerateSupportBundleAction(
+	logger flog.Logger,
+	builder support.BundleBuilder,
+	lcc command.LimaCmdCreator,
+) *generateSupportBundleAction {
+	return &generateSupportBundleAction{
+		logger:  logger,
+		builder: builder,
+		lcc:     lcc,
+	}
+}
+
+func (gsa *generateSupportBundleAction) runAdapter(cmd *cobra.Command, args []string) error {
+	additionalFiles, err := cmd.Flags().GetStringArray("include")
+	if err != nil {
+		return err
+	}
+	excludeFiles, err := cmd.Flags().GetStringArray("exclude")
+	if err != nil {
+		return err
+	}
+	return gsa.run(additionalFiles, excludeFiles)
+}
+
+func (gsa *generateSupportBundleAction) run(additionalFiles []string, excludeFiles []string) error {
+	err := gsa.assertVMExists()
+	if err != nil {
+		return err
+	}
+	gsa.logger.Info("Generating support bundle...")
+	bundleFile, err := gsa.builder.GenerateSupportBundle(additionalFiles, excludeFiles)
+	if err != nil {
+		return err
+	}
+	gsa.logger.Infof("Bundle created: %s", bundleFile)
+	gsa.logger.Info("Files posted on a Github issue can be read by anyone.")
+	gsa.logger.Info("Please ensure there is no sensitive information in the bundle before uploading.")
+	gsa.logger.Info("By default, this bundle contains basic logs and configs for Finch.")
+	return nil
+}
+
+func (gsa *generateSupportBundleAction) assertVMExists() error {
+	status, err := lima.GetVMStatus(gsa.lcc, gsa.logger, limaInstanceName)
+	if err != nil {
+		return err
+	}
+	switch status {
+	case lima.Nonexistent:
+		return fmt.Errorf("cannot create support bundle for nonexistent VM, run `finch %s init` to create a new instance",
+			virtualMachineRootCmd)
+	default:
+		return nil
+	}
+}
diff --git a/cmd/finch/support_bundle_test.go b/cmd/finch/support_bundle_test.go
new file mode 100644
index 000000000..34605c6bb
--- /dev/null
+++ b/cmd/finch/support_bundle_test.go
@@ -0,0 +1,332 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package main
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/golang/mock/gomock"
+	"github.com/stretchr/testify/assert"
+
+	"github.com/runfinch/finch/pkg/mocks"
+)
+
+func TestNewSupportBundleCommand(t *testing.T) {
+	t.Parallel()
+
+	cmd := newSupportBundleCommand(nil, nil, nil)
+	assert.Equal(t, cmd.Name(), "support-bundle")
+}
+
+func TestNewSupportBundleGenerateCommand(t *testing.T) {
+	t.Parallel()
+
+	cmd := newSupportBundleGenerateCommand(nil, nil, nil)
+	assert.Equal(t, cmd.Name(), "generate")
+}
+
+func TestGenerateSupportBundleAction_runAdapter(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		name    string
+		args    []string
+		mockSvc func(*gomock.Controller, *mocks.Logger, *mocks.BundleBuilder, *mocks.LimaCmdCreator)
+	}{
+		{
+			name: "no flags",
+			args: []string{},
+			mockSvc: func(
+				ctrl *gomock.Controller,
+				logger *mocks.Logger,
+				builder *mocks.BundleBuilder,
+				lcc *mocks.LimaCmdCreator,
+			) {
+				getVMStatusC := mocks.NewCommand(ctrl)
+				lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC)
+				getVMStatusC.EXPECT().Output().Return([]byte("Running"), nil)
+				logger.EXPECT().Debugf(gomock.Any(), gomock.Any())
+
+				logger.EXPECT().Info(gomock.Any())
+				builder.EXPECT().GenerateSupportBundle([]string{}, []string{}).Return("bundleName", nil)
+				logger.EXPECT().Infof(gomock.Any(), gomock.Any())
+				logger.EXPECT().Info(gomock.Any())
+				logger.EXPECT().Info(gomock.Any())
+				logger.EXPECT().Info(gomock.Any())
+			},
+		},
+		{
+			name: "one include flag",
+			args: []string{
+				"--include",
+				"testfile",
+			},
+			mockSvc: func(
+				ctrl *gomock.Controller,
+				logger *mocks.Logger,
+				builder *mocks.BundleBuilder,
+				lcc *mocks.LimaCmdCreator,
+			) {
+				getVMStatusC := mocks.NewCommand(ctrl)
+				lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC)
+				getVMStatusC.EXPECT().Output().Return([]byte("Running"), nil)
+				logger.EXPECT().Debugf(gomock.Any(), gomock.Any())
+
+				logger.EXPECT().Info(gomock.Any())
+				builder.EXPECT().GenerateSupportBundle([]string{"testfile"}, []string{}).Return("bundleName", nil)
+				logger.EXPECT().Infof(gomock.Any(), gomock.Any())
+				logger.EXPECT().Info(gomock.Any())
+				logger.EXPECT().Info(gomock.Any())
+				logger.EXPECT().Info(gomock.Any())
+			},
+		},
+		{
+			name: "multiple include flags",
+			args: []string{
+				"--include",
+				"testfile",
+				"--include",
+				"secondfile",
+			},
+			mockSvc: func(
+				ctrl *gomock.Controller,
+				logger *mocks.Logger,
+				builder *mocks.BundleBuilder,
+				lcc *mocks.LimaCmdCreator,
+			) {
+				getVMStatusC := mocks.NewCommand(ctrl)
+				lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC)
+				getVMStatusC.EXPECT().Output().Return([]byte("Running"), nil)
+				logger.EXPECT().Debugf(gomock.Any(), gomock.Any())
+
+				logger.EXPECT().Info(gomock.Any())
+				builder.EXPECT().GenerateSupportBundle([]string{"testfile", "secondfile"}, []string{}).Return("bundleName", nil)
+				logger.EXPECT().Infof(gomock.Any(), gomock.Any())
+				logger.EXPECT().Info(gomock.Any())
+				logger.EXPECT().Info(gomock.Any())
+				logger.EXPECT().Info(gomock.Any())
+			},
+		},
+		{
+			name: "one exclude flag",
+			args: []string{
+				"--exclude",
+				"testfile",
+			},
+			mockSvc: func(
+				ctrl *gomock.Controller,
+				logger *mocks.Logger,
+				builder *mocks.BundleBuilder,
+				lcc *mocks.LimaCmdCreator,
+			) {
+				getVMStatusC := mocks.NewCommand(ctrl)
+				lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC)
+				getVMStatusC.EXPECT().Output().Return([]byte("Running"), nil)
+				logger.EXPECT().Debugf(gomock.Any(), gomock.Any())
+
+				logger.EXPECT().Info(gomock.Any())
+				builder.EXPECT().GenerateSupportBundle([]string{}, []string{"testfile"}).Return("bundleName", nil)
+				logger.EXPECT().Infof(gomock.Any(), gomock.Any())
+				logger.EXPECT().Info(gomock.Any())
+				logger.EXPECT().Info(gomock.Any())
+				logger.EXPECT().Info(gomock.Any())
+			},
+		},
+		{
+			name: "multiple exclude flags",
+			args: []string{
+				"--exclude",
+				"testfile",
+				"--exclude",
+				"secondfile",
+			},
+			mockSvc: func(
+				ctrl *gomock.Controller,
+				logger *mocks.Logger,
+				builder *mocks.BundleBuilder,
+				lcc *mocks.LimaCmdCreator,
+			) {
+				getVMStatusC := mocks.NewCommand(ctrl)
+				lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC)
+				getVMStatusC.EXPECT().Output().Return([]byte("Running"), nil)
+				logger.EXPECT().Debugf(gomock.Any(), gomock.Any())
+
+				logger.EXPECT().Info(gomock.Any())
+				builder.EXPECT().GenerateSupportBundle([]string{}, []string{"testfile", "secondfile"}).Return("bundleName", nil)
+				logger.EXPECT().Infof(gomock.Any(), gomock.Any())
+				logger.EXPECT().Info(gomock.Any())
+				logger.EXPECT().Info(gomock.Any())
+				logger.EXPECT().Info(gomock.Any())
+			},
+		},
+		{
+			name: "combo of include and exclude flags",
+			args: []string{
+				"--include",
+				"testfile",
+				"--exclude",
+				"secondfile",
+			},
+			mockSvc: func(
+				ctrl *gomock.Controller,
+				logger *mocks.Logger,
+				builder *mocks.BundleBuilder,
+				lcc *mocks.LimaCmdCreator,
+			) {
+				getVMStatusC := mocks.NewCommand(ctrl)
+				lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC)
+				getVMStatusC.EXPECT().Output().Return([]byte("Running"), nil)
+				logger.EXPECT().Debugf(gomock.Any(), gomock.Any())
+
+				logger.EXPECT().Info(gomock.Any())
+				builder.EXPECT().GenerateSupportBundle([]string{"testfile"}, []string{"secondfile"}).Return("bundleName", nil)
+				logger.EXPECT().Infof(gomock.Any(), gomock.Any())
+				logger.EXPECT().Info(gomock.Any())
+				logger.EXPECT().Info(gomock.Any())
+				logger.EXPECT().Info(gomock.Any())
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			ctrl := gomock.NewController(t)
+			logger := mocks.NewLogger(ctrl)
+			builder := mocks.NewBundleBuilder(ctrl)
+			lcc := mocks.NewLimaCmdCreator(ctrl)
+			tc.mockSvc(ctrl, logger, builder, lcc)
+
+			cmd := newSupportBundleGenerateCommand(logger, builder, lcc)
+			cmd.SetArgs(tc.args)
+			assert.NoError(t, cmd.Execute())
+		})
+	}
+}
+
+func TestGenerateSupportBundleAction_run(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		name    string
+		wantErr error
+		mockSvc func(*gomock.Controller, *mocks.Logger, *mocks.BundleBuilder, *mocks.LimaCmdCreator, []string, []string)
+		include []string
+		exclude []string
+	}{
+		{
+			name:    "VM is running, no error",
+			wantErr: nil,
+			mockSvc: func(
+				ctrl *gomock.Controller,
+				logger *mocks.Logger,
+				builder *mocks.BundleBuilder,
+				lcc *mocks.LimaCmdCreator,
+				include []string,
+				exclude []string,
+			) {
+				getVMStatusC := mocks.NewCommand(ctrl)
+				lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC)
+				getVMStatusC.EXPECT().Output().Return([]byte("Running"), nil)
+				logger.EXPECT().Debugf("Status of virtual machine: %s", "Running")
+
+				logger.EXPECT().Info("Generating support bundle...")
+				builder.EXPECT().GenerateSupportBundle(include, exclude).Return("bundleName", nil)
+				logger.EXPECT().Infof("Bundle created: %s", "bundleName")
+				logger.EXPECT().Info("Files posted on a Github issue can be read by anyone.")
+				logger.EXPECT().Info("Please ensure there is no sensitive information in the bundle before uploading.")
+				logger.EXPECT().Info("By default, this bundle contains basic logs and configs for Finch.")
+			},
+			include: []string{},
+			exclude: []string{},
+		},
+		{
+			name:    "VM is stopped, no error",
+			wantErr: nil,
+			mockSvc: func(
+				ctrl *gomock.Controller,
+				logger *mocks.Logger,
+				builder *mocks.BundleBuilder,
+				lcc *mocks.LimaCmdCreator,
+				include []string,
+				exclude []string,
+			) {
+				getVMStatusC := mocks.NewCommand(ctrl)
+				lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC)
+				getVMStatusC.EXPECT().Output().Return([]byte("Stopped"), nil)
+				logger.EXPECT().Debugf("Status of virtual machine: %s", "Stopped")
+
+				logger.EXPECT().Info("Generating support bundle...")
+				builder.EXPECT().GenerateSupportBundle(include, exclude).Return("bundleName", nil)
+				logger.EXPECT().Infof("Bundle created: %s", "bundleName")
+				logger.EXPECT().Info("Files posted on a Github issue can be read by anyone.")
+				logger.EXPECT().Info("Please ensure there is no sensitive information in the bundle before uploading.")
+				logger.EXPECT().Info("By default, this bundle contains basic logs and configs for Finch.")
+			},
+			include: []string{},
+			exclude: []string{},
+		},
+		{
+			name: "VM is nonexistent",
+			wantErr: fmt.Errorf("cannot create support bundle for nonexistent VM, run `finch %s init` to create a new instance",
+				virtualMachineRootCmd),
+			mockSvc: func(
+				ctrl *gomock.Controller,
+				logger *mocks.Logger,
+				builder *mocks.BundleBuilder,
+				lcc *mocks.LimaCmdCreator,
+				include []string,
+				exclude []string,
+			) {
+				getVMStatusC := mocks.NewCommand(ctrl)
+				lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC)
+				getVMStatusC.EXPECT().Output().Return([]byte(""), nil)
+				logger.EXPECT().Debugf("Status of virtual machine: %s", "")
+			},
+			include: []string{},
+			exclude: []string{},
+		},
+		{
+			name:    "VM is running, error generating bundle",
+			wantErr: fmt.Errorf("foo"),
+			mockSvc: func(
+				ctrl *gomock.Controller,
+				logger *mocks.Logger,
+				builder *mocks.BundleBuilder,
+				lcc *mocks.LimaCmdCreator,
+				include []string,
+				exclude []string,
+			) {
+				getVMStatusC := mocks.NewCommand(ctrl)
+				lcc.EXPECT().CreateWithoutStdio("ls", "-f", "{{.Status}}", limaInstanceName).Return(getVMStatusC)
+				getVMStatusC.EXPECT().Output().Return([]byte("Running"), nil)
+				logger.EXPECT().Debugf("Status of virtual machine: %s", "Running")
+
+				logger.EXPECT().Info("Generating support bundle...")
+				builder.EXPECT().GenerateSupportBundle(include, exclude).Return("", fmt.Errorf("foo"))
+			},
+			include: []string{},
+			exclude: []string{},
+		},
+	}
+
+	for _, tc := range testCases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			ctrl := gomock.NewController(t)
+			logger := mocks.NewLogger(ctrl)
+			builder := mocks.NewBundleBuilder(ctrl)
+			lcc := mocks.NewLimaCmdCreator(ctrl)
+
+			tc.mockSvc(ctrl, logger, builder, lcc, tc.include, tc.exclude)
+			err := newGenerateSupportBundleAction(logger, builder, lcc).run(tc.include, tc.exclude)
+			assert.Equal(t, tc.wantErr, err)
+		})
+	}
+}
diff --git a/e2e/vm/support_bundle_test.go b/e2e/vm/support_bundle_test.go
new file mode 100644
index 000000000..da906a457
--- /dev/null
+++ b/e2e/vm/support_bundle_test.go
@@ -0,0 +1,308 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package vm
+
+import (
+	"archive/zip"
+	"fmt"
+	"os"
+	"path"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"github.com/onsi/ginkgo/v2"
+	"github.com/onsi/gomega"
+	"github.com/runfinch/common-tests/command"
+	"github.com/runfinch/common-tests/option"
+)
+
+var testSupportBundle = func(o *option.Option) {
+	ginkgo.Describe("Support bundles", func() {
+		ginkgo.It("Should generate a support bundle", func() {
+			command.Run(o, "support-bundle", "generate")
+			entries, err := os.ReadDir(".")
+			gomega.Expect(err).Should(gomega.BeNil())
+			bundleExists := false
+			for _, dirEntry := range entries {
+				if strings.Contains(dirEntry.Name(), "finch-support") {
+					_, err := os.Stat(dirEntry.Name())
+					if err == nil {
+						bundleExists = true
+					}
+
+					err = os.Remove(dirEntry.Name())
+					gomega.Expect(err).Should(gomega.BeNil())
+				}
+			}
+			gomega.Expect(bundleExists).Should(gomega.BeTrue())
+		})
+		ginkgo.It("Should generate a support bundle with an extra file included with --include flag by relative path", func() {
+			includeFilename := fmt.Sprintf("tempTestfile%s", time.Now().Format("20060102150405"))
+			//nolint:gosec // this file is only used for testing purposes and it does not include any user input
+			_, err := os.Create(includeFilename)
+			gomega.Expect(err).Should(gomega.BeNil())
+			defer func() {
+				err := os.Remove(includeFilename)
+				gomega.Expect(err).Should(gomega.BeNil())
+			}()
+
+			command.Run(o, "support-bundle", "generate", "--include", includeFilename)
+			entries, err := os.ReadDir(".")
+			gomega.Expect(err).Should(gomega.BeNil())
+			bundleExists := false
+			for _, dirEntry := range entries {
+				if strings.Contains(dirEntry.Name(), "finch-support") {
+					_, err := os.Stat(dirEntry.Name())
+					if err == nil {
+						bundleExists = true
+					}
+
+					reader, err := zip.OpenReader(dirEntry.Name())
+					gomega.Expect(err).Should(gomega.BeNil())
+
+					zipBaseName := path.Base(dirEntry.Name())
+					zipPrefix := strings.TrimSuffix(zipBaseName, path.Ext(zipBaseName))
+					_, err = reader.Open(path.Join(zipPrefix, "misc", includeFilename))
+					gomega.Expect(err).Should(gomega.BeNil())
+
+					err = os.Remove(dirEntry.Name())
+					gomega.Expect(err).Should(gomega.BeNil())
+				}
+			}
+			gomega.Expect(bundleExists).Should(gomega.BeTrue())
+		})
+		ginkgo.It("Should generate a support bundle with an extra file included with --include flag by absolute path", func() {
+			includeFilename := fmt.Sprintf("tempTestfile%s", time.Now().Format("20060102150405"))
+			//nolint:gosec // this file is only used for testing purposes and it does not include any user input
+			_, err := os.Create(includeFilename)
+			gomega.Expect(err).Should(gomega.BeNil())
+			defer func() {
+				err := os.Remove(includeFilename)
+				gomega.Expect(err).Should(gomega.BeNil())
+			}()
+
+			dir, err := os.Getwd()
+			gomega.Expect(err).Should(gomega.BeNil())
+			includeAbsPath := path.Join(dir, includeFilename)
+
+			command.Run(o, "support-bundle", "generate", "--include", includeAbsPath)
+			entries, err := os.ReadDir(".")
+			gomega.Expect(err).Should(gomega.BeNil())
+			bundleExists := false
+			for _, dirEntry := range entries {
+				if strings.Contains(dirEntry.Name(), "finch-support") {
+					_, err := os.Stat(dirEntry.Name())
+					if err == nil {
+						bundleExists = true
+					}
+
+					reader, err := zip.OpenReader(dirEntry.Name())
+					gomega.Expect(err).Should(gomega.BeNil())
+
+					zipBaseName := path.Base(dirEntry.Name())
+					zipPrefix := strings.TrimSuffix(zipBaseName, path.Ext(zipBaseName))
+					_, err = reader.Open(path.Join(zipPrefix, "misc", includeFilename))
+					gomega.Expect(err).Should(gomega.BeNil())
+
+					err = os.Remove(dirEntry.Name())
+					gomega.Expect(err).Should(gomega.BeNil())
+				}
+			}
+			gomega.Expect(bundleExists).Should(gomega.BeTrue())
+		})
+		ginkgo.It("Should generate a support bundle with no extra file included with --include flag but an invalid path", func() {
+			fakeFileName := "test123+fakefile"
+			command.Run(o, "support-bundle", "generate", "--include", fakeFileName)
+			entries, err := os.ReadDir(".")
+			gomega.Expect(err).Should(gomega.BeNil())
+			bundleExists := false
+			for _, dirEntry := range entries {
+				if strings.Contains(dirEntry.Name(), "finch-support") {
+					_, err := os.Stat(dirEntry.Name())
+					if err == nil {
+						bundleExists = true
+					}
+
+					reader, err := zip.OpenReader(dirEntry.Name())
+					gomega.Expect(err).Should(gomega.BeNil())
+
+					zipBaseName := path.Base(dirEntry.Name())
+					zipPrefix := strings.TrimSuffix(zipBaseName, path.Ext(zipBaseName))
+					_, err = reader.Open(path.Join(zipPrefix, "misc", fakeFileName))
+					gomega.Expect(err).ShouldNot(gomega.BeNil())
+
+					err = os.Remove(dirEntry.Name())
+					gomega.Expect(err).Should(gomega.BeNil())
+				}
+			}
+			gomega.Expect(bundleExists).Should(gomega.BeTrue())
+		})
+		ginkgo.It("Should generate a support bundle with a default file excluded with --exclude flag by basename", func() {
+			command.Run(o, "support-bundle", "generate", "--exclude", "serial.log")
+			entries, err := os.ReadDir(".")
+			gomega.Expect(err).Should(gomega.BeNil())
+			bundleExists := false
+			for _, dirEntry := range entries {
+				if strings.Contains(dirEntry.Name(), "finch-support") {
+					_, err := os.Stat(dirEntry.Name())
+					if err == nil {
+						bundleExists = true
+					}
+
+					reader, err := zip.OpenReader(dirEntry.Name())
+					gomega.Expect(err).Should(gomega.BeNil())
+
+					zipBaseName := path.Base(dirEntry.Name())
+					zipPrefix := strings.TrimSuffix(zipBaseName, path.Ext(zipBaseName))
+					_, err = reader.Open(path.Join(zipPrefix, "logs", "serial.log"))
+					gomega.Expect(err).ShouldNot(gomega.BeNil())
+
+					err = os.Remove(dirEntry.Name())
+					gomega.Expect(err).Should(gomega.BeNil())
+				}
+			}
+			gomega.Expect(bundleExists).Should(gomega.BeTrue())
+		})
+		ginkgo.It("Should generate a support bundle with a default file excluded with --exclude flag by absolute path", func() {
+			absPath, err := filepath.Abs("../../_output/lima/data/finch/serial.log")
+			gomega.Expect(err).Should(gomega.BeNil())
+			command.Run(o, "support-bundle", "generate", "--exclude", absPath)
+			entries, err := os.ReadDir(".")
+			gomega.Expect(err).Should(gomega.BeNil())
+			bundleExists := false
+			for _, dirEntry := range entries {
+				if strings.Contains(dirEntry.Name(), "finch-support") {
+					_, err := os.Stat(dirEntry.Name())
+					if err == nil {
+						bundleExists = true
+					}
+
+					reader, err := zip.OpenReader(dirEntry.Name())
+					gomega.Expect(err).Should(gomega.BeNil())
+
+					zipBaseName := path.Base(dirEntry.Name())
+					zipPrefix := strings.TrimSuffix(zipBaseName, path.Ext(zipBaseName))
+					_, err = reader.Open(path.Join(zipPrefix, "logs", "serial.log"))
+					gomega.Expect(err).ShouldNot(gomega.BeNil())
+
+					err = os.Remove(dirEntry.Name())
+					gomega.Expect(err).Should(gomega.BeNil())
+				}
+			}
+			gomega.Expect(bundleExists).Should(gomega.BeTrue())
+		})
+		ginkgo.It("Should generate a support bundle with a default file excluded with --exclude flag by relative path", func() {
+			command.Run(o, "support-bundle", "generate", "--exclude", "../../_output/lima/data/finch/serial.log")
+			entries, err := os.ReadDir(".")
+			gomega.Expect(err).Should(gomega.BeNil())
+			bundleExists := false
+			for _, dirEntry := range entries {
+				if strings.Contains(dirEntry.Name(), "finch-support") {
+					_, err := os.Stat(dirEntry.Name())
+					if err == nil {
+						bundleExists = true
+					}
+
+					reader, err := zip.OpenReader(dirEntry.Name())
+					gomega.Expect(err).Should(gomega.BeNil())
+
+					zipBaseName := path.Base(dirEntry.Name())
+					zipPrefix := strings.TrimSuffix(zipBaseName, path.Ext(zipBaseName))
+					_, err = reader.Open(path.Join(zipPrefix, "logs", "serial.log"))
+					gomega.Expect(err).ShouldNot(gomega.BeNil())
+
+					err = os.Remove(dirEntry.Name())
+					gomega.Expect(err).Should(gomega.BeNil())
+				}
+			}
+			gomega.Expect(bundleExists).Should(gomega.BeTrue())
+		})
+		ginkgo.It("Should generate a support bundle with no file excluded with --exclude flag with invalid path", func() {
+			fakeFileName := "test123+fakefile"
+			command.Run(o, "support-bundle", "generate", "--exclude", fakeFileName)
+			entries, err := os.ReadDir(".")
+			gomega.Expect(err).Should(gomega.BeNil())
+			bundleExists := false
+			for _, dirEntry := range entries {
+				if strings.Contains(dirEntry.Name(), "finch-support") {
+					_, err := os.Stat(dirEntry.Name())
+					if err == nil {
+						bundleExists = true
+					}
+
+					reader, err := zip.OpenReader(dirEntry.Name())
+					gomega.Expect(err).Should(gomega.BeNil())
+
+					zipBaseName := path.Base(dirEntry.Name())
+					zipPrefix := strings.TrimSuffix(zipBaseName, path.Ext(zipBaseName))
+					_, err = reader.Open(path.Join(zipPrefix, "logs", "serial.log"))
+					gomega.Expect(err).Should(gomega.BeNil())
+
+					err = os.Remove(dirEntry.Name())
+					gomega.Expect(err).Should(gomega.BeNil())
+				}
+			}
+			gomega.Expect(bundleExists).Should(gomega.BeTrue())
+		})
+		ginkgo.It("Should generate a support bundle with a file excluded when specified with both --include and --exclude", func() {
+			includeFilename := fmt.Sprintf("tempTestfile%s", time.Now().Format("20060102150405"))
+			//nolint:gosec // this file is only used for testing purposes and it does not include any user input
+			_, err := os.Create(includeFilename)
+			gomega.Expect(err).Should(gomega.BeNil())
+			defer func() {
+				err := os.Remove(includeFilename)
+				gomega.Expect(err).Should(gomega.BeNil())
+			}()
+
+			command.Run(o, "support-bundle", "generate", "--include", includeFilename, "--exclude", includeFilename)
+			entries, err := os.ReadDir(".")
+			gomega.Expect(err).Should(gomega.BeNil())
+			bundleExists := false
+			for _, dirEntry := range entries {
+				if strings.Contains(dirEntry.Name(), "finch-support") {
+					_, err := os.Stat(dirEntry.Name())
+					if err == nil {
+						bundleExists = true
+					}
+
+					reader, err := zip.OpenReader(dirEntry.Name())
+					gomega.Expect(err).Should(gomega.BeNil())
+
+					zipBaseName := path.Base(dirEntry.Name())
+					zipPrefix := strings.TrimSuffix(zipBaseName, path.Ext(zipBaseName))
+					_, err = reader.Open(path.Join(zipPrefix, "misc", includeFilename))
+					gomega.Expect(err).ShouldNot(gomega.BeNil())
+
+					err = os.Remove(dirEntry.Name())
+					gomega.Expect(err).Should(gomega.BeNil())
+				}
+			}
+			gomega.Expect(bundleExists).Should(gomega.BeTrue())
+		})
+		ginkgo.It("Should fail to generate a support bundle when the VM is nonexistent", func() {
+			command.New(o, "vm", "stop").WithTimeoutInSeconds(90).Run()
+			command.New(o, "vm", "remove").WithTimeoutInSeconds(60).Run()
+			defer command.New(o, "vm", "init").WithTimeoutInSeconds(600).Run()
+
+			command.New(o, "support-bundle", "generate").WithoutSuccessfulExit().Run()
+
+			entries, err := os.ReadDir(".")
+			gomega.Expect(err).Should(gomega.BeNil())
+			bundleExists := false
+			for _, dirEntry := range entries {
+				if strings.Contains(dirEntry.Name(), "finch-support") {
+					_, err := os.Stat(dirEntry.Name())
+					if err == nil {
+						bundleExists = true
+					}
+
+					err = os.Remove(dirEntry.Name())
+					gomega.Expect(err).Should(gomega.BeNil())
+				}
+			}
+			gomega.Expect(bundleExists).Should(gomega.BeFalse())
+		})
+	})
+}
diff --git a/e2e/vm/vm_test.go b/e2e/vm/vm_test.go
index 59b6514e3..aab504866 100644
--- a/e2e/vm/vm_test.go
+++ b/e2e/vm/vm_test.go
@@ -48,6 +48,7 @@ func TestVM(t *testing.T) {
 		testFinchConfigFile(o)
 		testVersion(o)
 		testVirtualizationFrameworkAndRosetta(o, *e2e.Installed)
+		testSupportBundle(o)
 	})
 
 	gomega.RegisterFailHandler(ginkgo.Fail)
diff --git a/pkg/flog/log.go b/pkg/flog/log.go
index fdb09d2cd..30d4fc98e 100644
--- a/pkg/flog/log.go
+++ b/pkg/flog/log.go
@@ -14,6 +14,7 @@ type Logger interface {
 	Infof(format string, args ...interface{})
 	Infoln(args ...interface{})
 	Warnln(args ...interface{})
+	Warnf(format string, args ...interface{})
 	Error(args ...interface{})
 	Errorf(format string, args ...interface{})
 	Fatal(args ...interface{})
diff --git a/pkg/flog/logrus.go b/pkg/flog/logrus.go
index 4cb8d4a14..977211757 100644
--- a/pkg/flog/logrus.go
+++ b/pkg/flog/logrus.go
@@ -45,6 +45,11 @@ func (l *Logrus) Warnln(args ...interface{}) {
 	logrus.Warnln(args...)
 }
 
+// Warnf logs a message at level Warn.
+func (l *Logrus) Warnf(format string, args ...interface{}) {
+	logrus.Warnf(format, args...)
+}
+
 // Error logs a message at level Error.
 func (l *Logrus) Error(args ...interface{}) {
 	logrus.Error(args...)
diff --git a/pkg/mocks/logger.go b/pkg/mocks/logger.go
index 459ad9732..36f172250 100644
--- a/pkg/mocks/logger.go
+++ b/pkg/mocks/logger.go
@@ -180,6 +180,23 @@ func (mr *LoggerMockRecorder) SetLevel(arg0 interface{}) *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLevel", reflect.TypeOf((*Logger)(nil).SetLevel), arg0)
 }
 
+// Warnf mocks base method.
+func (m *Logger) Warnf(arg0 string, arg1 ...interface{}) {
+	m.ctrl.T.Helper()
+	varargs := []interface{}{arg0}
+	for _, a := range arg1 {
+		varargs = append(varargs, a)
+	}
+	m.ctrl.Call(m, "Warnf", varargs...)
+}
+
+// Warnf indicates an expected call of Warnf.
+func (mr *LoggerMockRecorder) Warnf(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	varargs := append([]interface{}{arg0}, arg1...)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warnf", reflect.TypeOf((*Logger)(nil).Warnf), varargs...)
+}
+
 // Warnln mocks base method.
 func (m *Logger) Warnln(arg0 ...interface{}) {
 	m.ctrl.T.Helper()
diff --git a/pkg/mocks/pkg_support_config.go b/pkg/mocks/pkg_support_config.go
new file mode 100644
index 000000000..f828d6135
--- /dev/null
+++ b/pkg/mocks/pkg_support_config.go
@@ -0,0 +1,62 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: pkg/support/config.go
+
+// Package mocks is a generated GoMock package.
+package mocks
+
+import (
+	reflect "reflect"
+
+	gomock "github.com/golang/mock/gomock"
+)
+
+// BundleConfig is a mock of BundleConfig interface.
+type BundleConfig struct {
+	ctrl     *gomock.Controller
+	recorder *BundleConfigMockRecorder
+}
+
+// BundleConfigMockRecorder is the mock recorder for BundleConfig.
+type BundleConfigMockRecorder struct {
+	mock *BundleConfig
+}
+
+// NewBundleConfig creates a new mock instance.
+func NewBundleConfig(ctrl *gomock.Controller) *BundleConfig {
+	mock := &BundleConfig{ctrl: ctrl}
+	mock.recorder = &BundleConfigMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *BundleConfig) EXPECT() *BundleConfigMockRecorder {
+	return m.recorder
+}
+
+// ConfigFiles mocks base method.
+func (m *BundleConfig) ConfigFiles() []string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ConfigFiles")
+	ret0, _ := ret[0].([]string)
+	return ret0
+}
+
+// ConfigFiles indicates an expected call of ConfigFiles.
+func (mr *BundleConfigMockRecorder) ConfigFiles() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigFiles", reflect.TypeOf((*BundleConfig)(nil).ConfigFiles))
+}
+
+// LogFiles mocks base method.
+func (m *BundleConfig) LogFiles() []string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "LogFiles")
+	ret0, _ := ret[0].([]string)
+	return ret0
+}
+
+// LogFiles indicates an expected call of LogFiles.
+func (mr *BundleConfigMockRecorder) LogFiles() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LogFiles", reflect.TypeOf((*BundleConfig)(nil).LogFiles))
+}
diff --git a/pkg/mocks/pkg_support_support.go b/pkg/mocks/pkg_support_support.go
new file mode 100644
index 000000000..cb33f540f
--- /dev/null
+++ b/pkg/mocks/pkg_support_support.go
@@ -0,0 +1,49 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: pkg/support/support.go
+
+// Package mocks is a generated GoMock package.
+package mocks
+
+import (
+	reflect "reflect"
+
+	gomock "github.com/golang/mock/gomock"
+)
+
+// BundleBuilder is a mock of BundleBuilder interface.
+type BundleBuilder struct {
+	ctrl     *gomock.Controller
+	recorder *BundleBuilderMockRecorder
+}
+
+// BundleBuilderMockRecorder is the mock recorder for BundleBuilder.
+type BundleBuilderMockRecorder struct {
+	mock *BundleBuilder
+}
+
+// NewBundleBuilder creates a new mock instance.
+func NewBundleBuilder(ctrl *gomock.Controller) *BundleBuilder {
+	mock := &BundleBuilder{ctrl: ctrl}
+	mock.recorder = &BundleBuilderMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *BundleBuilder) EXPECT() *BundleBuilderMockRecorder {
+	return m.recorder
+}
+
+// GenerateSupportBundle mocks base method.
+func (m *BundleBuilder) GenerateSupportBundle(arg0, arg1 []string) (string, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GenerateSupportBundle", arg0, arg1)
+	ret0, _ := ret[0].(string)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GenerateSupportBundle indicates an expected call of GenerateSupportBundle.
+func (mr *BundleBuilderMockRecorder) GenerateSupportBundle(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateSupportBundle", reflect.TypeOf((*BundleBuilder)(nil).GenerateSupportBundle), arg0, arg1)
+}
diff --git a/pkg/support/config.go b/pkg/support/config.go
new file mode 100644
index 000000000..b9c2076ef
--- /dev/null
+++ b/pkg/support/config.go
@@ -0,0 +1,44 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package support
+
+import (
+	"path"
+
+	fpath "github.com/runfinch/finch/pkg/path"
+)
+
+// BundleConfig provides methods that configure what is included in a support bundle.
+type BundleConfig interface {
+	LogFiles() []string
+	ConfigFiles() []string
+}
+
+type bundleConfig struct {
+	finch   fpath.Finch
+	homeDir string
+}
+
+// NewBundleConfig creates a new bundleConfig.
+func NewBundleConfig(finch fpath.Finch, homeDir string) BundleConfig {
+	return &bundleConfig{
+		finch:   finch,
+		homeDir: homeDir,
+	}
+}
+
+func (bc *bundleConfig) LogFiles() []string {
+	return []string{
+		path.Join(bc.finch.LimaInstancePath(), "ha.stderr.log"),
+		path.Join(bc.finch.LimaInstancePath(), "ha.stdout.log"),
+		path.Join(bc.finch.LimaInstancePath(), "serial.log"),
+	}
+}
+
+func (bc *bundleConfig) ConfigFiles() []string {
+	return []string{
+		path.Join(bc.finch.LimaInstancePath(), "lima.yaml"),
+		bc.finch.ConfigFilePath(bc.homeDir),
+	}
+}
diff --git a/pkg/support/config_test.go b/pkg/support/config_test.go
new file mode 100644
index 000000000..d0f3f5c23
--- /dev/null
+++ b/pkg/support/config_test.go
@@ -0,0 +1,45 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package support
+
+import (
+	"path"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	fpath "github.com/runfinch/finch/pkg/path"
+)
+
+func TestNewBundleConfig(t *testing.T) {
+	t.Parallel()
+
+	finch := fpath.Finch("/mockfinch")
+	homeDir := "/mockhome"
+	NewBundleConfig(finch, homeDir)
+}
+
+func TestBundleConfig_LogFiles(t *testing.T) {
+	t.Parallel()
+
+	finch := fpath.Finch("/mockfinch")
+	homeDir := "/mockhome"
+	config := NewBundleConfig(finch, homeDir)
+
+	for _, fileName := range config.LogFiles() {
+		assert.True(t, path.IsAbs(fileName))
+	}
+}
+
+func TestBundleConfig_ConfigFiles(t *testing.T) {
+	t.Parallel()
+
+	finch := fpath.Finch("/mockfinch")
+	homeDir := "/mockhome"
+	config := NewBundleConfig(finch, homeDir)
+
+	for _, fileName := range config.ConfigFiles() {
+		assert.True(t, path.IsAbs(fileName))
+	}
+}
diff --git a/pkg/support/redact.go b/pkg/support/redact.go
new file mode 100644
index 000000000..1e20bee13
--- /dev/null
+++ b/pkg/support/redact.go
@@ -0,0 +1,74 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package support
+
+import (
+	"regexp"
+
+	"github.com/runfinch/finch/pkg/path"
+)
+
+func redactFinchInstall(content []byte, finch path.Finch) (redacted []byte, err error) {
+	finchInstallMatcher, err := regexp.Compile(string(finch))
+	if err != nil {
+		return nil, err
+	}
+	redacted = finchInstallMatcher.ReplaceAll(content, []byte("<finch-install-location-elided>"))
+	return redacted, nil
+}
+
+func redactUsername(content []byte, username string) (redacted []byte, err error) {
+	usernameMatcher, err := regexp.Compile(username)
+	if err != nil {
+		return nil, err
+	}
+	redacted = usernameMatcher.ReplaceAll(content, []byte("<username-elided>"))
+	return redacted, nil
+}
+
+func redactNetworkAddresses(content []byte) []byte {
+	ipv4Matcher := regexp.MustCompile(`(?:[0-9]{1,3}\.){3}[0-9]{1,3}(:[0-9]{1,5})?`)
+	redacted := ipv4Matcher.ReplaceAll(content, []byte("<ip-address-elided>"))
+
+	ipv6Matcher := regexp.MustCompile(`(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}`)
+	redacted = ipv6Matcher.ReplaceAll(redacted, []byte("<ip-address-elided>"))
+
+	macMatcher := regexp.MustCompile(`([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})`)
+	redacted = macMatcher.ReplaceAll(redacted, []byte("<mac-address-elided>"))
+
+	return redacted
+}
+
+func redactSSHKeys(content []byte) []byte {
+	ecdsaMatcher := regexp.MustCompile(`ecdsa-sha2-nistp256 .* root@lima-finch`)
+	redacted := ecdsaMatcher.ReplaceAll(content, []byte("<key-elided>"))
+
+	ed25519Matcher := regexp.MustCompile(`ssh-ed25519 .* root@lima-finch`)
+	redacted = ed25519Matcher.ReplaceAll(redacted, []byte("<key-elided>"))
+
+	rsaMatcher := regexp.MustCompile(`ssh-rsa .* root@lima-finch`)
+	redacted = rsaMatcher.ReplaceAll(redacted, []byte("<key-elided>"))
+
+	return redacted
+}
+
+func redactPorts(content []byte) []byte {
+	// we can't redact every 1-5 digit number, so we have to redact based on specific context.
+	nonEmptyMatcher := regexp.MustCompile(`('\[' -n )[0-9]{1,5}( ']')`)
+	redacted := nonEmptyMatcher.ReplaceAll(content, []byte("$1<port-elided>$2"))
+
+	nonZeroMatcher := regexp.MustCompile(`('\[' )[0-9]{1,5}( -ne 0 ']')`)
+	redacted = nonZeroMatcher.ReplaceAll(redacted, []byte("$1<port-elided>$2"))
+
+	sshCommandMatcher := regexp.MustCompile(`(\[ssh -F .* -p )[0-9]{1,5}(.*])`)
+	redacted = sshCommandMatcher.ReplaceAll(redacted, []byte("$1<port-elided>$2"))
+
+	statusMatcher := regexp.MustCompile(`([{,]"sshLocalPort":)[0-9]{1,5}(})`)
+	redacted = statusMatcher.ReplaceAll(redacted, []byte("$1<port-elided>$2"))
+
+	portMatcher := regexp.MustCompile(`(port )[0-9]{1,5}`)
+	redacted = portMatcher.ReplaceAll(redacted, []byte("$1<port-elided>"))
+
+	return redacted
+}
diff --git a/pkg/support/redact_test.go b/pkg/support/redact_test.go
new file mode 100644
index 000000000..ecb91b3fa
--- /dev/null
+++ b/pkg/support/redact_test.go
@@ -0,0 +1,217 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package support
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/runfinch/finch/pkg/path"
+)
+
+func TestSupport_redactFinchInstall(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		name   string
+		input  []byte
+		expect []byte
+	}{
+		{
+			name:   "redact Finch install location",
+			input:  []byte("/Applications/Finch/"),
+			expect: []byte("<finch-install-location-elided>"),
+		},
+		{
+			name:   "install location redaction not needed",
+			input:  []byte("/Applications/Flinch/"),
+			expect: []byte("/Applications/Flinch/"),
+		},
+	}
+
+	for _, tc := range testCases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			out, err := redactFinchInstall(tc.input, path.Finch("/Applications/Finch/"))
+			assert.NoError(t, err)
+			assert.Equal(t, tc.expect, out)
+		})
+	}
+}
+
+func TestSupport_redactUsername(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		name   string
+		input  []byte
+		expect []byte
+	}{
+		{
+			name:   "redact username",
+			input:  []byte("this is foo_user's username: foo_user"),
+			expect: []byte("this is <username-elided>'s username: <username-elided>"),
+		},
+		{
+			name:   "username redaction not needed",
+			input:  []byte("this will not include a username"),
+			expect: []byte("this will not include a username"),
+		},
+	}
+
+	for _, tc := range testCases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			out, err := redactUsername(tc.input, "foo_user")
+			assert.NoError(t, err)
+			assert.Equal(t, tc.expect, out)
+		})
+	}
+}
+
+func TestSupport_redactNetworkAddresses(t *testing.T) {
+	t.Parallel()
+
+	timestamp := time.Now().String()
+
+	testCases := []struct {
+		name   string
+		input  []byte
+		expect []byte
+	}{
+		{
+			name:   "redact ipv4 address",
+			input:  []byte("127.0.0.1"),
+			expect: []byte("<ip-address-elided>"),
+		},
+		{
+			name:   "redact ipv4 address with port",
+			input:  []byte("192.168.5.4:12497"),
+			expect: []byte("<ip-address-elided>"),
+		},
+		{
+			name:   "redact ipv6 address",
+			input:  []byte("DEAD:BEEF:0123:4567:89AB:CDEF:DEAD:BEEF"),
+			expect: []byte("<ip-address-elided>"),
+		},
+		{
+			name:   "redact mac address",
+			input:  []byte("de:ad:be:ef:01:23"),
+			expect: []byte("<mac-address-elided>"),
+		},
+		{
+			name:   "do not redact timestamps",
+			input:  []byte(timestamp),
+			expect: []byte(timestamp),
+		},
+		{
+			name:   "do not redact seconds",
+			input:  []byte("1.234567"),
+			expect: []byte("1.234567"),
+		},
+	}
+
+	for _, tc := range testCases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			out := redactNetworkAddresses(tc.input)
+			assert.Equal(t, tc.expect, out)
+		})
+	}
+}
+
+func TestSupport_redactSSHKeys(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		name   string
+		input  []byte
+		expect []byte
+	}{
+		{
+			name:   "should replace ecdsa key",
+			input:  []byte("ecdsa-sha2-nistp256 AAA/BBB/CCC...xyz= root@lima-finch"),
+			expect: []byte("<key-elided>"),
+		},
+		{
+			name:   "should replace ed25519 key",
+			input:  []byte("ssh-ed25519 AAABBBCCC...xyz root@lima-finch"),
+			expect: []byte("<key-elided>"),
+		},
+		{
+			name:   "should replace rsa key",
+			input:  []byte("ssh-rsa AAABBBCCC12/abc+...xyz= root@lima-finch"),
+			expect: []byte("<key-elided>"),
+		},
+	}
+
+	for _, tc := range testCases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			out := redactSSHKeys(tc.input)
+			assert.Equal(t, tc.expect, out)
+		})
+	}
+}
+
+func TestSupport_redactPorts(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		name   string
+		input  []byte
+		expect []byte
+	}{
+		{
+			name:   "should replace in non-empty test statement",
+			input:  []byte("'[' -n 12345 ']'"),
+			expect: []byte("'[' -n <port-elided> ']'"),
+		},
+		{
+			name:   "should replace in non-zero test statement",
+			input:  []byte("'[' 1 -ne 0 ']'"),
+			expect: []byte("'[' <port-elided> -ne 0 ']'"),
+		},
+		{
+			name:   "should replace in ssh command",
+			input:  []byte("[ssh -F /dev/null ... -p 12 ... -- /bin/bash]"),
+			expect: []byte("[ssh -F /dev/null ... -p <port-elided> ... -- /bin/bash]"),
+		},
+		{
+			name:   "should replace in ha.stdout.log status",
+			input:  []byte("{\"time\":\"2023-03-16T15:51:17.188418-07:00\",\"status\":{\"running\":true,\"sshLocalPort\":1234}}"),
+			expect: []byte("{\"time\":\"2023-03-16T15:51:17.188418-07:00\",\"status\":{\"running\":true,\"sshLocalPort\":<port-elided>}}"),
+		},
+		{
+			name:   "should replace when preceded by port",
+			input:  []byte("port 123"),
+			expect: []byte("port <port-elided>"),
+		},
+		{
+			name:   "should not replace 1-5 digit number",
+			input:  []byte("12345"),
+			expect: []byte("12345"),
+		},
+	}
+
+	for _, tc := range testCases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			out := redactPorts(tc.input)
+			assert.Equal(t, tc.expect, out)
+		})
+	}
+}
diff --git a/pkg/support/support.go b/pkg/support/support.go
new file mode 100644
index 000000000..bd5149269
--- /dev/null
+++ b/pkg/support/support.go
@@ -0,0 +1,297 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+// Package support provides functions and methods to produce Finch support bundles
+package support
+
+import (
+	"archive/zip"
+	"bytes"
+	"errors"
+	"fmt"
+	"io"
+	"path"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"github.com/lima-vm/lima/pkg/osutil"
+	"github.com/spf13/afero"
+	"gopkg.in/yaml.v3"
+
+	"github.com/runfinch/finch/pkg/command"
+	"github.com/runfinch/finch/pkg/flog"
+	fpath "github.com/runfinch/finch/pkg/path"
+	"github.com/runfinch/finch/pkg/version"
+)
+
+const (
+	bundlePrefix     = "finch-support"
+	platformFileName = "platform.yaml"
+	logPrefix        = "logs"
+	configPrefix     = "configs"
+	additionalPrefix = "misc"
+)
+
+// PlatformData defines the YAML structure for the platform data included in a support bundle.
+type PlatformData struct {
+	Os    string `yaml:"os"`
+	Arch  string `yaml:"arch"`
+	Finch string `yaml:"finch"`
+}
+
+// BundleBuilder provides methods to generate support bundles.
+type BundleBuilder interface {
+	GenerateSupportBundle([]string, []string) (string, error)
+}
+
+type bundleBuilder struct {
+	logger flog.Logger
+	fs     afero.Fs
+	config BundleConfig
+	finch  fpath.Finch
+	ecc    command.Creator
+}
+
+// NewBundleBuilder produces a new BundleBuilder.
+func NewBundleBuilder(
+	logger flog.Logger,
+	fs afero.Fs,
+	config BundleConfig,
+	finch fpath.Finch,
+	ecc command.Creator,
+) BundleBuilder {
+	return &bundleBuilder{
+		logger: logger,
+		fs:     fs,
+		config: config,
+		finch:  finch,
+		ecc:    ecc,
+	}
+}
+
+// GenerateSupportBundle generates a new support bundle.
+func (bb *bundleBuilder) GenerateSupportBundle(additionalFiles []string, excludeFiles []string) (string, error) {
+	zipFileName := bundleFileName()
+	bb.logger.Debugf("Creating %s...", zipFileName)
+	zipFile, err := bb.fs.Create(zipFileName)
+	if err != nil {
+		return "", err
+	}
+
+	zipPrefix := strings.TrimSuffix(zipFileName, path.Ext(zipFileName))
+
+	writer := zip.NewWriter(zipFile)
+
+	_, err = writer.Create(fmt.Sprintf("%s/", zipPrefix))
+	if err != nil {
+		return "", err
+	}
+
+	platform, err := bb.getPlatformData()
+	if err != nil {
+		return "", err
+	}
+
+	bb.logger.Debugln("Gathering platform data...")
+	err = writePlatformData(writer, platform, zipPrefix)
+	if err != nil {
+		return "", err
+	}
+
+	bb.logger.Debugln("Copying in log files...")
+	for _, file := range bb.config.LogFiles() {
+		if fileShouldBeExcluded(file, excludeFiles) {
+			bb.logger.Infof("Excluding %s...", file)
+			continue
+		}
+		err := bb.copyInFile(writer, file, path.Join(zipPrefix, logPrefix))
+		if err != nil {
+			bb.logger.Warnf("Could not copy in %q. Error: %s", file, err)
+		}
+	}
+
+	bb.logger.Debugln("Copying in config files...")
+	for _, file := range bb.config.ConfigFiles() {
+		if fileShouldBeExcluded(file, excludeFiles) {
+			bb.logger.Infof("Excluding %s...", file)
+			continue
+		}
+		err := bb.copyInFile(writer, file, path.Join(zipPrefix, configPrefix))
+		if err != nil {
+			bb.logger.Warnf("Could not copy in %q. Error: %s", file, err)
+		}
+	}
+
+	bb.logger.Debugln("Copying in additional files...")
+	for _, file := range additionalFiles {
+		if fileShouldBeExcluded(file, excludeFiles) {
+			bb.logger.Infof("Excluding %s...", file)
+			continue
+		}
+		err := bb.copyInFile(writer, file, path.Join(zipPrefix, additionalPrefix))
+		if err != nil {
+			bb.logger.Warnf("Could not add additional file %s. Error: %s", file, err)
+		}
+	}
+
+	err = writer.Close()
+	if err != nil {
+		return "", err
+	}
+
+	return zipFileName, nil
+}
+
+func (bb *bundleBuilder) copyInFile(writer *zip.Writer, fileName string, prefix string) error {
+	f, err := bb.fs.Open(fileName)
+	if err != nil {
+		return err
+	}
+
+	bb.logger.Debugf("Copying %s...", fileName)
+
+	var buf bytes.Buffer
+	_, err = buf.ReadFrom(f)
+	if err != nil {
+		return err
+	}
+
+	var redacted []byte
+	var bufErr error
+	for bufErr == nil {
+		var line []byte
+		line, bufErr = buf.ReadBytes('\n')
+		if bufErr != nil && !errors.Is(bufErr, io.EOF) {
+			continue
+		}
+
+		line, err = redactFinchInstall(line, bb.finch)
+		if err != nil {
+			return err
+		}
+
+		user, err := osutil.LimaUser(false)
+		if err != nil {
+			return err
+		}
+		line, err = redactUsername(line, user.Username)
+		if err != nil {
+			return err
+		}
+
+		line = redactNetworkAddresses(line)
+		line = redactPorts(line)
+		line = redactSSHKeys(line)
+
+		redacted = append(redacted, line...)
+	}
+
+	baseName := path.Base(fileName)
+	zipCopy, err := writer.Create(path.Join(prefix, baseName))
+	if err != nil {
+		return err
+	}
+
+	_, err = zipCopy.Write(redacted)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (bb *bundleBuilder) getPlatformData() (*PlatformData, error) {
+	platform := &PlatformData{}
+
+	// populate OS version
+	os, err := bb.getOSVersion()
+	if err != nil {
+		return nil, err
+	}
+	platform.Os = os
+
+	// populate arch
+	arch, err := bb.getArch()
+	if err != nil {
+		return nil, err
+	}
+	platform.Arch = arch
+
+	// populate Finch version
+	platform.Finch = getFinchVersion()
+
+	return platform, nil
+}
+
+func (bb *bundleBuilder) getOSVersion() (string, error) {
+	cmd := bb.ecc.Create("sw_vers", "-productVersion")
+	out, err := cmd.Output()
+	if err != nil {
+		return "", err
+	}
+
+	os := strings.TrimSuffix(string(out), "\n")
+
+	return os, nil
+}
+
+func (bb *bundleBuilder) getArch() (string, error) {
+	cmd := bb.ecc.Create("uname", "-m")
+	out, err := cmd.Output()
+	if err != nil {
+		return "", err
+	}
+
+	arch := strings.TrimSuffix(string(out), "\n")
+
+	return arch, nil
+}
+
+func getFinchVersion() string {
+	return version.Version
+}
+
+func writePlatformData(writer *zip.Writer, platform *PlatformData, prefix string) error {
+	platformFile, err := writer.Create(path.Join(prefix, platformFileName))
+	if err != nil {
+		return err
+	}
+
+	platformYaml, err := yaml.Marshal(&platform)
+	if err != nil {
+		return err
+	}
+
+	_, err = platformFile.Write(platformYaml)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func bundleFileName() string {
+	timestamp := time.Now().Format("20060102150405")
+	return fmt.Sprintf("%s-%s.zip", bundlePrefix, timestamp)
+}
+
+func fileShouldBeExcluded(filename string, exclude []string) bool {
+	fileAbs, err := filepath.Abs(filename)
+	if err != nil {
+		return true
+	}
+	for _, excludeFile := range exclude {
+		excludeAbs, err := filepath.Abs(excludeFile)
+		if err != nil {
+			continue
+		}
+		if fileAbs == excludeAbs {
+			return true
+		}
+		if path.Base(filename) == excludeFile {
+			return true
+		}
+	}
+	return false
+}
diff --git a/pkg/support/support_test.go b/pkg/support/support_test.go
new file mode 100644
index 000000000..f512fcdb6
--- /dev/null
+++ b/pkg/support/support_test.go
@@ -0,0 +1,343 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package support
+
+import (
+	"archive/zip"
+	"testing"
+	"time"
+
+	"github.com/golang/mock/gomock"
+	"github.com/spf13/afero"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/runfinch/finch/pkg/mocks"
+	fpath "github.com/runfinch/finch/pkg/path"
+)
+
+func TestSupport_NewBundleBuilder(t *testing.T) {
+	t.Parallel()
+
+	ctrl := gomock.NewController(t)
+	ecc := mocks.NewCommandCreator(ctrl)
+	logger := mocks.NewLogger(ctrl)
+	fs := afero.NewMemMapFs()
+	finch := fpath.Finch("mockfinch")
+
+	config := NewBundleConfig(finch, "mockhome")
+	NewBundleBuilder(logger, fs, config, finch, ecc)
+}
+
+func TestSupportBundleBuilder_GenerateSupportBundle(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		name    string
+		mockSvc func(*mocks.Logger, *mocks.BundleConfig, *mocks.CommandCreator, *mocks.Command)
+		include []string
+		exclude []string
+	}{
+		{
+			name: "Generate support bundle",
+			mockSvc: func(logger *mocks.Logger, config *mocks.BundleConfig, ecc *mocks.CommandCreator, cmd *mocks.Command) {
+				logger.EXPECT().Debugf("Creating %s...", gomock.Any())
+				logger.EXPECT().Debugln("Gathering platform data...")
+
+				ecc.EXPECT().Create("sw_vers", "-productVersion").Return(cmd)
+				cmd.EXPECT().Output().Return([]byte("1.2.3\n"), nil)
+				ecc.EXPECT().Create("uname", "-m").Return(cmd)
+				cmd.EXPECT().Output().Return([]byte("arch\n"), nil)
+
+				config.EXPECT().LogFiles().Return([]string{
+					"log1",
+					"log2",
+				})
+
+				config.EXPECT().ConfigFiles().Return([]string{
+					"config1",
+					"config2",
+				})
+
+				logger.EXPECT().Debugln("Copying in log files...")
+				logger.EXPECT().Debugf("Copying %s...", "log1")
+				logger.EXPECT().Debugf("Copying %s...", "log2")
+				logger.EXPECT().Debugln("Copying in config files...")
+				logger.EXPECT().Debugf("Copying %s...", "config1")
+				logger.EXPECT().Debugf("Copying %s...", "config2")
+				logger.EXPECT().Debugln("Copying in additional files...")
+			},
+			include: []string{},
+			exclude: []string{},
+		},
+		{
+			name: "Generate support bundle with an extra file included",
+			mockSvc: func(logger *mocks.Logger, config *mocks.BundleConfig, ecc *mocks.CommandCreator, cmd *mocks.Command) {
+				logger.EXPECT().Debugf("Creating %s...", gomock.Any())
+				logger.EXPECT().Debugln("Gathering platform data...")
+
+				ecc.EXPECT().Create("sw_vers", "-productVersion").Return(cmd)
+				cmd.EXPECT().Output().Return([]byte("1.2.3\n"), nil)
+				ecc.EXPECT().Create("uname", "-m").Return(cmd)
+				cmd.EXPECT().Output().Return([]byte("arch\n"), nil)
+
+				config.EXPECT().LogFiles().Return([]string{
+					"log1",
+				})
+
+				config.EXPECT().ConfigFiles().Return([]string{
+					"config1",
+				})
+
+				logger.EXPECT().Debugln("Copying in log files...")
+				logger.EXPECT().Debugf("Copying %s...", "log1")
+				logger.EXPECT().Debugln("Copying in config files...")
+				logger.EXPECT().Debugf("Copying %s...", "config1")
+				logger.EXPECT().Debugln("Copying in additional files...")
+				logger.EXPECT().Debugf("Copying %s...", "extra1")
+			},
+			include: []string{"extra1"},
+			exclude: []string{},
+		},
+		{
+			name: "Generate support bundle with a log file excluded",
+			mockSvc: func(logger *mocks.Logger, config *mocks.BundleConfig, ecc *mocks.CommandCreator, cmd *mocks.Command) {
+				logger.EXPECT().Debugf("Creating %s...", gomock.Any())
+				logger.EXPECT().Debugln("Gathering platform data...")
+
+				ecc.EXPECT().Create("sw_vers", "-productVersion").Return(cmd)
+				cmd.EXPECT().Output().Return([]byte("1.2.3\n"), nil)
+				ecc.EXPECT().Create("uname", "-m").Return(cmd)
+				cmd.EXPECT().Output().Return([]byte("arch\n"), nil)
+
+				config.EXPECT().LogFiles().Return([]string{
+					"log1",
+				})
+
+				config.EXPECT().ConfigFiles().Return([]string{
+					"config1",
+				})
+
+				logger.EXPECT().Debugln("Copying in log files...")
+				logger.EXPECT().Infof("Excluding %s...", "log1")
+				logger.EXPECT().Debugln("Copying in config files...")
+				logger.EXPECT().Debugf("Copying %s...", "config1")
+				logger.EXPECT().Debugln("Copying in additional files...")
+			},
+			include: []string{},
+			exclude: []string{"log1"},
+		},
+		{
+			name: "Generate support bundle with a config file excluded",
+			mockSvc: func(logger *mocks.Logger, config *mocks.BundleConfig, ecc *mocks.CommandCreator, cmd *mocks.Command) {
+				logger.EXPECT().Debugf("Creating %s...", gomock.Any())
+				logger.EXPECT().Debugln("Gathering platform data...")
+
+				ecc.EXPECT().Create("sw_vers", "-productVersion").Return(cmd)
+				cmd.EXPECT().Output().Return([]byte("1.2.3\n"), nil)
+				ecc.EXPECT().Create("uname", "-m").Return(cmd)
+				cmd.EXPECT().Output().Return([]byte("arch\n"), nil)
+
+				config.EXPECT().LogFiles().Return([]string{
+					"log1",
+				})
+
+				config.EXPECT().ConfigFiles().Return([]string{
+					"config1",
+				})
+
+				logger.EXPECT().Debugln("Copying in log files...")
+				logger.EXPECT().Debugf("Copying %s...", "log1")
+				logger.EXPECT().Debugln("Copying in config files...")
+				logger.EXPECT().Infof("Excluding %s...", "config1")
+				logger.EXPECT().Debugln("Copying in additional files...")
+			},
+			include: []string{},
+			exclude: []string{"config1"},
+		},
+		{
+			name: "Generate support bundle with an included file excluded",
+			mockSvc: func(logger *mocks.Logger, config *mocks.BundleConfig, ecc *mocks.CommandCreator, cmd *mocks.Command) {
+				logger.EXPECT().Debugf("Creating %s...", gomock.Any())
+				logger.EXPECT().Debugln("Gathering platform data...")
+
+				ecc.EXPECT().Create("sw_vers", "-productVersion").Return(cmd)
+				cmd.EXPECT().Output().Return([]byte("1.2.3\n"), nil)
+				ecc.EXPECT().Create("uname", "-m").Return(cmd)
+				cmd.EXPECT().Output().Return([]byte("arch\n"), nil)
+
+				config.EXPECT().LogFiles().Return([]string{
+					"log1",
+				})
+
+				config.EXPECT().ConfigFiles().Return([]string{
+					"config1",
+				})
+
+				logger.EXPECT().Debugln("Copying in log files...")
+				logger.EXPECT().Debugf("Copying %s...", "log1")
+				logger.EXPECT().Debugln("Copying in config files...")
+				logger.EXPECT().Debugf("Copying %s...", "config1")
+				logger.EXPECT().Debugln("Copying in additional files...")
+				logger.EXPECT().Infof("Excluding %s...", "extra1")
+			},
+			include: []string{"extra1"},
+			exclude: []string{"extra1"},
+		},
+	}
+
+	for _, tc := range testCases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			ctrl := gomock.NewController(t)
+			fs := afero.NewMemMapFs()
+			logger := mocks.NewLogger(ctrl)
+			config := mocks.NewBundleConfig(ctrl)
+			finch := fpath.Finch("mockfinch")
+			ecc := mocks.NewCommandCreator(ctrl)
+			cmd := mocks.NewCommand(ctrl)
+
+			builder := &bundleBuilder{
+				logger: logger,
+				fs:     fs,
+				config: config,
+				finch:  finch,
+				ecc:    ecc,
+			}
+
+			testFiles := []string{
+				"log1", "log2", // "log" files
+				"config1", "config2", // "config" files
+				"extra1", // "additional" files
+			}
+
+			for _, fileName := range testFiles {
+				f, err := fs.Create(fileName)
+				require.NoError(t, err)
+
+				_, err = f.Write([]byte("file contents\n"))
+				require.NoError(t, err)
+
+				err = f.Close()
+				require.NoError(t, err)
+			}
+
+			tc.mockSvc(logger, config, ecc, cmd)
+
+			zipFile, err := builder.GenerateSupportBundle(tc.include, tc.exclude)
+			assert.NoError(t, err)
+
+			exists, err := afero.Exists(fs, zipFile)
+			assert.NoError(t, err)
+			assert.True(t, exists)
+		})
+	}
+}
+
+func TestSupport_writePlatformData(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		name    string
+		data    *PlatformData
+		wantErr error
+	}{
+		{
+			name: "successfully write platform data",
+			data: &PlatformData{
+				Os:    "12.5.3",
+				Arch:  "x86_64",
+				Finch: "0.5.0",
+			},
+			wantErr: nil,
+		},
+		{
+			name: "incomplete platform data",
+			data: &PlatformData{
+				Os: "12.5.3",
+			},
+			wantErr: nil,
+		},
+	}
+
+	for _, tc := range testCases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			fs := afero.NewMemMapFs()
+
+			file, err := fs.Create("testFile")
+			require.NoError(t, err)
+
+			writer := zip.NewWriter(file)
+
+			err = writePlatformData(writer, tc.data, bundlePrefix)
+			assert.Equal(t, tc.wantErr, err)
+
+			err = file.Close()
+			assert.NoError(t, err)
+		})
+	}
+}
+
+func TestSupport_bundleFileName(t *testing.T) {
+	t.Parallel()
+
+	first := bundleFileName()
+	time.Sleep(time.Second)
+	second := bundleFileName()
+
+	assert.Contains(t, first, bundlePrefix)
+	assert.Contains(t, second, bundlePrefix)
+	assert.NotEqual(t, first, second)
+}
+
+func TestSupport_fileShouldBeExcluded(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		name    string
+		file    string
+		exclude []string
+		result  bool
+	}{
+		{
+			name:    "full filename should be excluded",
+			file:    "/finch/lima/data/serial.log",
+			exclude: []string{"/finch/lima/data/serial.log"},
+			result:  true,
+		},
+		{
+			name:    "basename should be excluded",
+			file:    "/finch/lima/data/serial.log",
+			exclude: []string{"serial.log"},
+			result:  true,
+		},
+		{
+			name:    "empty exclude list",
+			file:    "/finch/lima/data/serial.log",
+			exclude: []string{},
+			result:  false,
+		},
+		{
+			name:    "file not in exclude list",
+			file:    "/finch/lima/data/serial.log",
+			exclude: []string{"other.file"},
+			result:  false,
+		},
+	}
+
+	for _, tc := range testCases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			result := fileShouldBeExcluded(tc.file, tc.exclude)
+			assert.Equal(t, tc.result, result)
+		})
+	}
+}