From 53503f98c469b90cd7abceaa2976659f8167d263 Mon Sep 17 00:00:00 2001 From: hudson-newey Date: Wed, 2 Oct 2024 22:38:44 +1000 Subject: [PATCH] Add support for --overwrite flag --- README.md | 30 +++++++++++++++++++-- src/config/parse_test.go | 3 +++ src/models/config.go | 32 ++++++++++++++++++++--- src/models/config_test.go | 22 ++++++++++++++++ src/patches/rm.go | 48 +++++++++++++++++++++++++++++++++- tests/assets/configs/valid.yml | 2 ++ 6 files changed, 131 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ee433e2..386c637 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,11 @@ "rm with guard rails" -Wraps the rm command with a more secure, safer, and more private version +A wrapper for the "rm" command with soft-deletes, config-based deletion, debug information, and saner defaults -## Command line arguments +## Additional command line arguments +- `--overwrite` Overwrite the disk location location with zeros - `--hard` Do not soft-delete file - `--soft` Soft delete a file. A backup will be stored in `/tmp/2rm` - `--silent` Do not print out additional information priduced by 2rm. This is useful for scripting situations @@ -42,6 +43,23 @@ By using the `/tmp` directory, the operating system will **automatically hard de Sometimes you want to hard delete a file/directory every time that you run the `rm` command e.g. you probably want your `node_modules` hard deleted every time and never want to soft delete them. In this case, you can modify your `~/.local/share/2rm/config.yml` file to always hard delete `node_modules`. +### Overwriting disk location with zeros + +When deleting a file with the linux inbuilt `rm` command, the file is still avaliable on disk. + +Meaning that the file can still be recovered by any sufficiantly technical user. + +This can be problematic when dealing with sensitive files such as private keys that if leaked could lead to catastrophic consequences. + +You can overwrite a files disk location (rendering it unrecoverable) by using the `--overwrite` flag. + +2rm will still soft-delete the file by default, but the soft-deleted file will be completely filled with zeros. + +I made the decision that overwritten files will still be soft deleted because it might be useful for timestamp logging/auditing purposes. +E.g. "when did I overwrite xyz" + +If you want to fully delete a file from disk and the file system use both the `--overwrite` and `--hard` flags. + ### Config-based deletion You can specify what directories are soft-deleted anb hard-deleted by using the `~/.local/share/2rm/config.yml` file. @@ -54,6 +72,14 @@ You can specify what directories are soft-deleted anb hard-deleted by using the # any files that are soft deleted will be # backed up in the `backups` directory backups: /tmp/2rm/ +# whenever files matching these paths are deleted +# the disk location will be overwritten with zeros +overwrite: + # when deleting ssh keys, we always want to + # overwrite them with zeros to protect + # against attackers recovering the production + # ssh keys + - ".ssh/*" hard: - "node_modules/" - "target/" diff --git a/src/config/parse_test.go b/src/config/parse_test.go index 7da43ab..9af0b9f 100644 --- a/src/config/parse_test.go +++ b/src/config/parse_test.go @@ -25,6 +25,9 @@ func assertConfig(t *testing.T, configPath string, expectedConfig models.Config) func TestParsingConfig(t *testing.T) { expectedConfig := models.Config{ Backups: "/tmp/2rm/", + Overwrite: []string{ + ".ssh/*", + }, Hard: []string{ "node_modules/", "target/", diff --git a/src/models/config.go b/src/models/config.go index 9758054..4fedf39 100644 --- a/src/models/config.go +++ b/src/models/config.go @@ -6,9 +6,24 @@ import ( ) type Config struct { - Backups string - Hard []string - Soft []string + // the backup location for soft-deletes + Backups string + + // any file paths that match these patterns will be overwritten with + // zeros when deleted + Overwrite []string + + // any file paths that match these patterns will be hard-deleted + Hard []string + + // any file paths that match these patterns will be soft-deleted + // soft-deletes take precedence over hard-deletes + // meaning that if a file matches both a hard and soft delete pattern + // the file will be soft-deleted + Soft []string + + // any file paths that match these patterns will be protected from deletion + // protected files cannot be deleted without the --bypass-protected flag Protected []string } @@ -34,6 +49,17 @@ func (config Config) ShouldSoftDelete(path string) bool { return false } +func (config Config) ShouldOverwrite(path string) bool { + for _, overwritePath := range config.Overwrite { + matched := matchesPattern(overwritePath, path) + if matched { + return true + } + } + + return false +} + func (config Config) IsProtected(path string) bool { return util.InArray(config.Protected, path) } diff --git a/src/models/config_test.go b/src/models/config_test.go index 5e59f37..f3be77c 100644 --- a/src/models/config_test.go +++ b/src/models/config_test.go @@ -122,3 +122,25 @@ func TestNotProtected(t *testing.T) { t.Fatalf("Expected %v but got %v", expected, realized) } } + +func TestShouldOverwrite(t *testing.T) { + testedConfig := loadConfig("valid.yml") + + expected := true + realized := testedConfig.ShouldOverwrite(".ssh/test.pem") + + if expected != realized { + t.Fatalf("Expected %v but got %v", expected, realized) + } +} + +func TestNotShouldOverwrite(t *testing.T) { + testedConfig := loadConfig("valid.yml") + + expected := false + realized := testedConfig.ShouldOverwrite("non-existent.txt") + + if expected != realized { + t.Fatalf("Expected %v but got %v", expected, realized) + } +} diff --git a/src/patches/rm.go b/src/patches/rm.go index 2609911..84c7342 100644 --- a/src/patches/rm.go +++ b/src/patches/rm.go @@ -18,6 +18,7 @@ const SOFT_DELETE_CLA = "--soft" const SILENT_CLA = "--silent" const DRY_RUN_CLA = "--dry-run" const BYPASS_PROTECTED_CLA = "--bypass-protected" +const OVERWRITE_CLA = "--overwrite" func RmPatch(arguments []string, config models.Config) { forceHardDelete := util.InArray(arguments, HARD_DELETE_CLA) @@ -25,6 +26,7 @@ func RmPatch(arguments []string, config models.Config) { silent := util.InArray(arguments, SILENT_CLA) dryRun := util.InArray(arguments, DRY_RUN_CLA) bypassProtected := util.InArray(arguments, BYPASS_PROTECTED_CLA) + overwrite := util.InArray(arguments, OVERWRITE_CLA) actionedArgs := removeUnNeededArguments( removeDangerousArguments(arguments), @@ -65,6 +67,18 @@ func RmPatch(arguments []string, config models.Config) { isConfigHardDelete := config.ShouldHardDelete(absolutePath) isConfigSoftDelete := config.ShouldSoftDelete(absolutePath) + // overwriting a file is not exclusive to hard/soft deletes + // meaning that you can overwrite the contents of a file with zeros and + // also soft delete it + // I have made this decision because I think soft-deleting an + // overwritten file has auditing/logging use cases + // e.g. Who deleted this file? When was it deleted? + // if we hard deleted the file, we would lose this information + isConfigOverwrite := config.ShouldOverwrite(absolutePath) + if overwrite || isConfigOverwrite { + overwriteFile(absolutePath) + } + if isTmp || forceHardDelete || isConfigHardDelete && !isConfigSoftDelete && !forceSoftDelete { hardDelete([]string{path}, extractedArguments) } else { @@ -91,8 +105,16 @@ func shouldPassthrough(arguments []string) bool { } func removeUnNeededArguments(arguments []string) []string { - unNeededArguments := []string{"-r", HARD_DELETE_CLA, SOFT_DELETE_CLA, SILENT_CLA} returnedArguments := []string{} + unNeededArguments := []string{ + "-r", + HARD_DELETE_CLA, + SOFT_DELETE_CLA, + SILENT_CLA, + DRY_RUN_CLA, + BYPASS_PROTECTED_CLA, + OVERWRITE_CLA, + } for _, arg := range arguments { if !util.InArray(unNeededArguments, arg) { @@ -205,3 +227,27 @@ func hardDelete(filePaths []string, arguments []string) { command := "rm -r " + strings.Join(arguments, " ") + " " + strings.Join(filePaths, " ") commands.Execute(command) } + +func overwriteFile(filePath string) { + file, err := os.OpenFile(filePath, os.O_RDWR, 0644) + if err != nil { + fmt.Println("Error opening file:", err) + os.Exit(2) + } + defer file.Close() + + fileInfo, err := file.Stat() + if err != nil { + fmt.Println("Error getting file info:", err) + os.Exit(2) + } + + fileSize := fileInfo.Size() + zeroBytes := make([]byte, fileSize) + + _, err = file.WriteAt(zeroBytes, 0) + if err != nil { + fmt.Println("Error writing to file:", err) + os.Exit(2) + } +} diff --git a/tests/assets/configs/valid.yml b/tests/assets/configs/valid.yml index 3065423..b328712 100644 --- a/tests/assets/configs/valid.yml +++ b/tests/assets/configs/valid.yml @@ -1,4 +1,6 @@ backups: /tmp/2rm/ +overwrite: + - ".ssh/*" hard: - "node_modules/" - "target/"