diff --git a/VERSION b/VERSION index 4a30051..7a4bbaa 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.139 +0.3.140 diff --git a/internal/api/silence.go b/internal/api/silence.go new file mode 100644 index 0000000..6089f31 --- /dev/null +++ b/internal/api/silence.go @@ -0,0 +1,19 @@ +package api + +import ( + "cli/internal/common" +) + +func GetSilencedMap(silenced []SilenceRule, allDependencies common.DependencyMap, manager string) map[string][]string { + silencedPackages := make(map[string][]string) + for _, rule := range silenced { + ruleDependencyId := common.DependencyId(manager, rule.Library, rule.Version) + silencedPaths := []string{} + for _, dep := range allDependencies[ruleDependencyId] { + silencedPaths = append(silencedPaths, dep.DiskPath) + } + silencedPackages[ruleDependencyId] = silencedPaths + } + + return silencedPackages +} diff --git a/internal/common/fs.go b/internal/common/fs.go index 0bc48e6..635de99 100644 --- a/internal/common/fs.go +++ b/internal/common/fs.go @@ -178,17 +178,12 @@ func ConvertSymLinkToFile(path string) error { return nil } - opts := copy.Options{ - PreserveTimes: true, - PreserveOwner: true, - } - if err := os.Remove(path); err != nil { slog.Error("failed removing symlink", "err", err, "path", path) return err } - if err := copy.Copy(resolvedPath, path, opts); err != nil { + if err := CopyFile(resolvedPath, path); err != nil { slog.Error("failed converting symlink to file", "err", err, "path", resolvedPath) return err } @@ -210,3 +205,17 @@ func ListDir(path string) ([]string, error) { return res, nil } + +func CopyFile(srcPath string, dstPath string) error { + opts := copy.Options{ + PreserveTimes: true, + PreserveOwner: true, + } + + err := copy.Copy(srcPath, dstPath, opts) + if err != nil { + slog.Error("failed copying file", "err", err, "src", srcPath, "dst", dstPath) + } + + return err +} diff --git a/internal/common/rename.go b/internal/common/rename.go index 9b415f4..cbc1378 100644 --- a/internal/common/rename.go +++ b/internal/common/rename.go @@ -5,8 +5,6 @@ import ( "log/slog" "os" "syscall" - - "github.com/otiai10/copy" ) // tries to use os.Rename first @@ -36,13 +34,7 @@ func Move(source, destination string) error { slog.Debug("Cross-device link detected (EXDEV).") // Handle cross-device move logic here (e.g., copy and delete) - // Copy the file. - opts := copy.Options{ - PreserveTimes: true, - PreserveOwner: true, - } - - if err := copy.Copy(source, destination, opts); err != nil { + if err := CopyFile(source, destination); err != nil { slog.Error("copy failed", "err", err, "src", source, "dst", destination) if rmErr := os.RemoveAll(destination); rmErr != nil { // attempting to clean if copy failed midway, nothing we can do if this fails diff --git a/internal/ecosystem/apk/apk/manager.go b/internal/ecosystem/apk/apk/manager.go index 37e4f19..81da5fd 100644 --- a/internal/ecosystem/apk/apk/manager.go +++ b/internal/ecosystem/apk/apk/manager.go @@ -151,7 +151,6 @@ func (m *APKPackageManager) NormalizePackageName(name string) string { } func (m *APKPackageManager) SilencePackages(silenceArray []api.SilenceRule, allDependencies common.DependencyMap) (map[string][]string, error) { - silencedPackages := make(map[string][]string) dbContent, err := os.ReadFile(utils.ApkDBPath) if err != nil { slog.Error("failed to silence package", "err", err) @@ -159,6 +158,7 @@ func (m *APKPackageManager) SilencePackages(silenceArray []api.SilenceRule, allD } var wasRenamed bool + silenced := []api.SilenceRule{} newDBContent := string(dbContent) for _, rule := range silenceArray { wasRenamed, newDBContent = utils.RenamePackage(newDBContent, rule) @@ -168,13 +168,7 @@ func (m *APKPackageManager) SilencePackages(silenceArray []api.SilenceRule, allD continue } - ruleDependencyId := common.DependencyId(mappings.ApkManager, rule.Library, rule.Version) - - silencedPaths := []string{} - for _, dep := range allDependencies[ruleDependencyId] { - silencedPaths = append(silencedPaths, dep.DiskPath) - } - silencedPackages[ruleDependencyId] = silencedPaths + silenced = append(silenced, rule) } err = common.DumpBytes(utils.ApkDBPath, []byte(newDBContent)) @@ -183,7 +177,7 @@ func (m *APKPackageManager) SilencePackages(silenceArray []api.SilenceRule, allD return nil, err } - return silencedPackages, nil + return api.GetSilencedMap(silenced, allDependencies, mappings.ApkManager), nil } func (m *APKPackageManager) ConsolidateVulnerabilities(vulnerablePackages *[]api.PackageVersion, allDependencies common.DependencyMap) (*[]api.PackageVersion, error) { diff --git a/internal/ecosystem/golang/fixer.go b/internal/ecosystem/golang/fixer.go index c157ba9..780c98e 100644 --- a/internal/ecosystem/golang/fixer.go +++ b/internal/ecosystem/golang/fixer.go @@ -5,7 +5,6 @@ import ( "bytes" "cli/internal/common" "cli/internal/ecosystem/shared" - "fmt" "log/slog" "os" "path/filepath" @@ -31,6 +30,19 @@ type fixer struct { workdir string vendorAlreadyExists bool vendorDir string + tmpGoModPath string +} + +func (f *fixer) saveGoModFile() error { + origModPath := filepath.Join(f.projectDir, "go.mod") + tmpGoModPath := filepath.Join(f.workdir, "go.mod") + err := common.CopyFile(origModPath, tmpGoModPath) + if err != nil { + return err + } + + f.tmpGoModPath = tmpGoModPath + return nil } // Run `go mod vendor` to create a vendor directory with all dependencies @@ -42,31 +54,18 @@ func (f *fixer) Prepare() error { return err } - f.vendorDir = filepath.Join(f.projectDir, vendorDir) - exists, err := common.DirExists(f.vendorDir) + err := f.saveGoModFile() if err != nil { - slog.Error("failed checking if vendor directory exists", "err", err) + slog.Error("failed copying go.mod file", "err", err) return err } - if exists { - slog.Info("vendor directory already exists, will not create", "vendorDir", f.vendorDir) - f.vendorAlreadyExists = true - return nil - } - - slog.Info("running go mod vendor", "vendorDir", f.vendorDir) - pr, err := common.RunCmdWithArgs(f.projectDir, "go", "mod", "vendor") + err = PrepareVendorDir(f.projectDir) if err != nil { - slog.Error("failed running go mod vendor", "err", err) - return err - } - if pr.Code != 0 { - slog.Error("running go mod vendor returned non-zero", "result", pr) - return fmt.Errorf("running go mod vendor returned non-zero") + slog.Error("failed preparing vendor dir", "err", err) } - return nil + return err } // files in zip include the module's version, but should appear without it in the vendor folder @@ -119,6 +118,15 @@ func (f *fixer) Fix(entry shared.DependencyDescriptor, dep *common.Dependency, p // If it already existed before the fix, rollback each dependency to previous state // Otherwise, remove it entirely func (f *fixer) Rollback() bool { + success := true + + goModPath := filepath.Join(f.projectDir, goModFilename) + err := common.CopyFile(f.tmpGoModPath, goModPath) + if err != nil { + slog.Error("failed rolling back go.mod file", "err", err) // Try and rollback the other changes + success = false + } + if !f.vendorAlreadyExists { // remove `vendor` folder entirely slog.Info("rollback, removing vendor directory", "vendorDir", f.vendorDir) @@ -132,15 +140,17 @@ func (f *fixer) Rollback() bool { for orig, tmp := range f.rollback { if err := os.RemoveAll(orig); err != nil { slog.Error("failed removing original version dir", "dir", orig) + success = false } if err := common.Move(tmp, orig); err != nil { slog.Error("failed renaming tmp to original version dir", "tmp", tmp, "orig", orig) + success = false } } } - return true + return success } // Remove workdir @@ -153,10 +163,12 @@ func (f *fixer) Cleanup() bool { return true } -func NewFixer(projectDir string, workdir string) shared.DependencyFixer { +func newFixer(projectDir string, workdir string, vendorDirPath string, vendorAlreadyExists bool) shared.DependencyFixer { return &fixer{ - projectDir: projectDir, - workdir: workdir, - rollback: make(map[string]string, 100), + projectDir: projectDir, + workdir: workdir, + rollback: make(map[string]string, 100), + vendorDir: vendorDirPath, + vendorAlreadyExists: vendorAlreadyExists, } } diff --git a/internal/ecosystem/golang/manager.go b/internal/ecosystem/golang/manager.go index 80735f1..1aeb7c2 100644 --- a/internal/ecosystem/golang/manager.go +++ b/internal/ecosystem/golang/manager.go @@ -24,14 +24,29 @@ const goExe = "go" const MinimalSupportedVersion = "1.17.0" type GolangPackageManager struct { - Config *config.Config - golangTargetFile string - targetDir string - goMod *modfile.File + Config *config.Config + golangTargetFile string + targetDir string + goMod *modfile.File + vendorDir string + vendorAlreadyExists bool } func NewGolangManager(config *config.Config, targetFile string, targetDir string) *GolangPackageManager { - return &GolangPackageManager{Config: config, golangTargetFile: targetFile, targetDir: targetDir} + vendorDirPath := filepath.Join(targetDir, vendorDir) + vendorAlreadyExists, err := isVendorDirExist(targetDir) + if err != nil { + slog.Error("failed checking vendor dir exists", "err", err) + return nil + } + + return &GolangPackageManager{ + Config: config, + golangTargetFile: targetFile, + targetDir: targetDir, + vendorDir: vendorDirPath, + vendorAlreadyExists: vendorAlreadyExists, + } } func (m *GolangPackageManager) Name() string { @@ -100,7 +115,7 @@ func (m *GolangPackageManager) GetProjectName() string { } func (m *GolangPackageManager) GetFixer(workdir string) shared.DependencyFixer { - return NewFixer(m.targetDir, workdir) + return newFixer(m.targetDir, workdir, m.vendorDir, m.vendorAlreadyExists) } func (m *GolangPackageManager) GetEcosystem() string { @@ -116,9 +131,19 @@ func (m *GolangPackageManager) DownloadPackage(server api.ArtifactServer, descri } func (m *GolangPackageManager) HandleFixes(fixes []shared.DependencyDescriptor) error { - if m.Config.UseSealedNames { - slog.Warn("using sealed names in golang is not supported yet") + if !m.Config.UseSealedNames { + return nil } + + slog.Info("using sealed names") + for _, fix := range fixes { + err := renamePackage(m.vendorDir, fix.VulnerablePackage.Library.Name, fix.VulnerablePackage.Version) + if err != nil { + slog.Error("failed renaming package", "package", fix.VulnerablePackage.Library.Name, "version", fix.VulnerablePackage.Version, "err", err) + return err + } + } + return nil } @@ -157,8 +182,32 @@ func GetPackageManager(config *config.Config, targetDir string, targetFile strin } func (m *GolangPackageManager) SilencePackages(silenceArray []api.SilenceRule, allDependencies common.DependencyMap) (map[string][]string, error) { - slog.Warn("Silencing packages is not support for golang") - return nil, nil + exists, err := isVendorDirExist(m.vendorDir) + if err != nil { + slog.Error("failed checking vendor dir exists", "err", err) + return nil, err + } + + if !exists { + err := PrepareVendorDir(m.targetDir) // prepare if was not done already when applied fixes + if err != nil { + slog.Error("failed preparing vendor dir", "err", err) + return nil, err + } + } + + silenced := []api.SilenceRule{} + for _, rule := range silenceArray { + err = renamePackage(m.vendorDir, rule.Library, rule.Version) + if err != nil { + slog.Error("failed renaming package", "package", rule.Library, "version", rule.Version, "err", err) + break + } + + silenced = append(silenced, rule) + } + + return api.GetSilencedMap(silenced, allDependencies, mappings.GolangManager), err } func (m *GolangPackageManager) ConsolidateVulnerabilities(vulnerablePackages *[]api.PackageVersion, allDependencies common.DependencyMap) (*[]api.PackageVersion, error) { diff --git a/internal/ecosystem/golang/silence.go b/internal/ecosystem/golang/silence.go new file mode 100644 index 0000000..2ead4f4 --- /dev/null +++ b/internal/ecosystem/golang/silence.go @@ -0,0 +1,106 @@ +package golang + +import ( + "cli/internal/common" + "fmt" + "log/slog" + "os" + "path/filepath" + "strings" +) + +const SealDir = "sealsecurity.io" + +func getReplaceString(packageName string, packageVersion string) string { + return fmt.Sprintf("-replace=%s@v%s=%s/%s@v%s", packageName, packageVersion, SealDir, packageName, packageVersion) +} + +func replaceInModFile(vendorDir string, packageName string, packageVersion string) error { + projectDir := filepath.Dir(vendorDir) + editOutput, err := common.RunCmdWithArgs(projectDir, goExe, "mod", "edit", getReplaceString(packageName, packageVersion)) // exists since go mod existed in go 1.11 + if err != nil { + slog.Error("failed running go mod edit", "err", err) + return err + } + + if editOutput.Code != 0 { + slog.Error("running go mod edit returned non-zero", "result", editOutput, "exitcode", editOutput.Code) + return fmt.Errorf("running go mod edit returned non-zero") + } + + return nil +} + +func modulesContentAddReplace(modulesFile string, packageName string, packageVersion string) (error, string) { + oldString := fmt.Sprintf("# %s v%s\n", packageName, packageVersion) + if count := strings.Count(modulesFile, oldString); count != 1 { + slog.Error("unexpected number of occurrences of package in modules.txt", "package", packageName, "version", packageVersion, "count", count) + return fmt.Errorf("unexpected number of occurrences of package in modules.txt"), "" + } + + newString := fmt.Sprintf("# %s v%s => %s/%s v%s\n", packageName, packageVersion, SealDir, packageName, packageVersion) + return nil, strings.Replace(modulesFile, oldString, newString, 1) +} + +func modifyModulesFile(vendorDir, packageName string, packageVersion string) error { + modulesFile, err := os.ReadFile(filepath.Join(vendorDir, "modules.txt")) + if err != nil { + slog.Error("failed reading modules.txt", "err", err) + return err + } + + err, newModulesContent := modulesContentAddReplace(string(modulesFile), packageName, packageVersion) + if err != nil { + return err + } + + err = os.WriteFile(filepath.Join(vendorDir, "modules.txt"), []byte(newModulesContent), os.ModePerm) + if err != nil { + slog.Error("failed writing modules.txt", "err", err) + } + + return err +} + +// Symlinks the package directory to a seal directory in the vendor directory +// Both directories need to appear in the vendor directory +// Otherwise go build won't work +// before: +// +// vendor/google.com/protobuf +// +// after +// +// vendor/google.com/protobuf +// vendor/sealsecurity.io/google.com/protobuf -> vendor/google.com/protobuf +func moveToVendorSealDir(vendorDir, packageName string) error { + newPackageDir := filepath.Join(vendorDir, SealDir, packageName) + err := os.MkdirAll(filepath.Dir(newPackageDir), os.ModePerm) + if err != nil { + slog.Error("failed creating seal directory", "err", err) + return err + } + + oldPackageDir := filepath.Join(vendorDir, packageName) + err = os.Symlink(oldPackageDir, newPackageDir) + if err != nil { + slog.Error("failed moving package to seal directory", "err", err) + return err + } + + return nil +} + +func renamePackage(vendorDir string, packageName string, packageVersion string) error { + err := replaceInModFile(vendorDir, packageName, packageVersion) + if err != nil { + return err + } + + err = modifyModulesFile(vendorDir, packageName, packageVersion) + if err != nil { + return err + } + + return moveToVendorSealDir(vendorDir, packageName) +} diff --git a/internal/ecosystem/golang/silence_test.go b/internal/ecosystem/golang/silence_test.go new file mode 100644 index 0000000..d8b9e58 --- /dev/null +++ b/internal/ecosystem/golang/silence_test.go @@ -0,0 +1,64 @@ +package golang + +import ( + "testing" +) + +func TestGetReplaceString(t *testing.T) { + replaceString := getReplaceString("github.com/Masterminds/semver", "1.5.0") + expected := "-replace=github.com/Masterminds/semver@v1.5.0=sealsecurity.io/github.com/Masterminds/semver@v1.5.0" + if replaceString != expected { + t.Fatalf("unexpected replace string: got %s expected %s", replaceString, expected) + } +} + +func TestModulesContentAddReplaceSanity(t *testing.T) { + content := `# github.com/Masterminds/semver v1.5.0 +## explicit +github.com/Masterminds/semver +` + err, modifiedContent := modulesContentAddReplace(content, "github.com/Masterminds/semver", "1.5.0") + if err != nil { + t.Fatalf("failed modifying content: %v", err) + } + + expectedContent := `# github.com/Masterminds/semver v1.5.0 => sealsecurity.io/github.com/Masterminds/semver v1.5.0 +## explicit +github.com/Masterminds/semver +` + + if modifiedContent != expectedContent { + t.Fatalf("unexpected content: got %s expected %s", modifiedContent, expectedContent) + } +} + +func TestModulesContentAddReplaceNoModule(t *testing.T) { + content := `# github.com/Masterminds/semver v1.2.3 +## explicit +github.com/Masterminds/semver +` + + err, modifiedContent := modulesContentAddReplace(content, "github.com/Masterminds/semver", "1.5.0") + if err == nil { + t.Fatalf("expected error") + } + + if modifiedContent != "" { + t.Fatalf("unexpected content: got %s", modifiedContent) + } +} + +func TestModulesContentAddReplaceMultipleModules(t *testing.T) { + content := `# github.com/Masterminds/semver v1.5.0 +## explicit +github.com/Masterminds/semver # github.com/Masterminds/semver v1.5.0 +` + err, modifiedContent := modulesContentAddReplace(content, "github.com/Masterminds/semver", "1.5.0") + if err == nil { + t.Fatalf("expected error") + } + + if modifiedContent != "" { + t.Fatalf("unexpected content: got %s", modifiedContent) + } +} diff --git a/internal/ecosystem/golang/utils.go b/internal/ecosystem/golang/utils.go index 84375a4..4b81f0f 100644 --- a/internal/ecosystem/golang/utils.go +++ b/internal/ecosystem/golang/utils.go @@ -1,7 +1,36 @@ package golang -import "strings" +import ( + "cli/internal/common" + "fmt" + "log/slog" + "path/filepath" + "strings" +) func NormalizePackageName(name string) string { return strings.ToLower(name) } + +func isVendorDirExist(projectDir string) (bool, error) { + vendorDir := filepath.Join(projectDir, "vendor") + return common.DirExists(vendorDir) +} + +// Run `go mod vendor` to create a vendor directory with all dependencies +// do nothing if it exists +func PrepareVendorDir(projectDir string) error { + slog.Info("running go mod vendor", "projectDir", projectDir) + pr, err := common.RunCmdWithArgs(projectDir, goExe, "mod", "vendor") + if err != nil { + slog.Error("failed running go mod vendor", "err", err) + return err + } + + if pr.Code != 0 { + slog.Error("running go mod vendor returned non-zero", "result", pr) + return fmt.Errorf("running go mod vendor returned non-zero") + } + + return nil +}