diff --git a/.gitignore b/.gitignore index 5b90e79..994ac50 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ *.dll *.so *.dylib - +logdebarker # Test binary, built with `go test -c` *.test diff --git a/README.md b/README.md index 36cbd0d..bf7ee39 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,85 @@ # logdebarker +`logdebarker` is a UNIX-style log sanitizer tool written in Go. It censors sensitive information from logs, config files, and other text streams based on user-defined patterns. This is useful for cleaning data before exporting logs or sharing files. + +## Features + +* Accepts input from STDIN or a file +* Outputs to STDOUT or a specified output file +* Uses `$HOME/.blocked_words.txt` as a pattern file for matching sensitive strings +* Enforces strict file permissions on the config file (`0700` only) +* Supports custom redaction strings (e.g. `redaction: `) +* Ignores comment lines beginning with `#` in both input and config + +## Usage + +Pipe logs through `logdebarker`: + +```sh +sudo cat /var/log/someprogram/log.log | logdebarker | tee cleaned_log.txt +``` + +Sanitize a file to a new output file: + +```sh +sudo logdebarker /etc/nginx/conf.d/somesite.conf output.txt +``` + +## Configuration + +`logdebarker` reads its blocklist from: + +``` +$HOME/.blocked_words.txt +``` + +Each line should be a literal string to redact. Lines starting with `#` are ignored. You may optionally define one redaction string: + +``` +redaction: [your-censor-string] +``` + +Example `.blocked_words.txt`: + +``` +# block API keys +redaction: +ABC123XYZ456 +supersecretpassword +``` + +## Security + +To protect your sensitive word list, `logdebarker` will refuse to run if `$HOME/.blocked_words.txt` is not `chmod 700`. + +```sh +chmod 700 ~/.blocked_words.txt +``` + +## Installation + +1. Clone and build: + +```sh +git clone https://your.git.repo/logdebarker.git +cd logdebarker +go build -o logdebarker +``` + +2. Move the binary to your PATH: + +```sh +sudo mv logdebarker /usr/local/bin/ +``` + +OR + +`go install archuser.org/logdebarker` + +## License + +This project is released under the GPLv3 License. + +--- + +For bug reports, suggestions, or contributions, open an issue or submit a pull request. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..14c3a4f --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module archuser.org/logdebarker + +go 1.24.3 diff --git a/main.go b/main.go new file mode 100644 index 0000000..9e9c682 --- /dev/null +++ b/main.go @@ -0,0 +1,132 @@ +package main + +import ( + "bufio" + "fmt" + "io" + "log" + "os" + "os/user" + "path/filepath" + "regexp" + "strings" +) + +var ( + blockedWords []*regexp.Regexp + redaction = "redacted" +) + +func checkBlockedWordsFilePerm(path string) { + info, err := os.Stat(path) + if err != nil { + log.Fatalf("Failed to stat %s: %v", path, err) + } + + mode := info.Mode().Perm() + if mode != 0o700 { + log.Fatalf("Permission on %s must be 700, found %o", path, mode) + } +} + +func loadBlockedWords(path string) { + file, err := os.Open(path) + if err != nil { + log.Fatalf("Failed to open %s: %v", path, err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + if strings.HasPrefix(line, "redaction:") { + if redaction != "redacted" { + log.Fatalf("Multiple redaction definitions found in %s", path) + } + redaction = strings.TrimSpace(strings.TrimPrefix(line, "redaction:")) + continue + } + blockedWords = append(blockedWords, regexp.MustCompile(regexp.QuoteMeta(line))) + } + if err := scanner.Err(); err != nil { + log.Fatalf("Error reading %s: %v", path, err) + } +} + +func censorLine(line string) string { + for _, re := range blockedWords { + line = re.ReplaceAllString(line, redaction) + } + return line +} + +func process(input io.Reader, output io.Writer) { + scanner := bufio.NewScanner(input) + writer := bufio.NewWriter(output) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(strings.TrimSpace(line), "#") { + fmt.Fprintln(writer, line) + continue + } + censored := censorLine(line) + fmt.Fprintln(writer, censored) + } + writer.Flush() + if err := scanner.Err(); err != nil { + log.Fatalf("Error reading input: %v", err) + } +} + +func main() { + if len(os.Args) == 1 && isInputFromTerminal() { + fmt.Fprintln(os.Stderr, "No input provided. Exiting.") + os.Exit(1) + } + + usr, err := user.Current() + if err != nil { + log.Fatalf("Unable to determine current user: %v", err) + } + confPath := filepath.Join(usr.HomeDir, ".blocked_words.txt") + checkBlockedWordsFilePerm(confPath) + loadBlockedWords(confPath) + + switch len(os.Args) { + case 1: + process(os.Stdin, os.Stdout) + case 2: + inFile, err := os.Open(os.Args[1]) + if err != nil { + log.Fatalf("Cannot open input file: %v", err) + } + defer inFile.Close() + process(inFile, os.Stdout) + case 3: + inFile, err := os.Open(os.Args[1]) + if err != nil { + log.Fatalf("Cannot open input file: %v", err) + } + defer inFile.Close() + outFile, err := os.Create(os.Args[2]) + if err != nil { + log.Fatalf("Cannot create output file: %v", err) + } + defer outFile.Close() + process(inFile, outFile) + default: + log.Fatalf("Usage: %s [input_file] [output_file]", os.Args[0]) + } +} + +func isInputFromTerminal() bool { + fileInfo, err := os.Stdin.Stat() + if err != nil { + return true + } + return (fileInfo.Mode() & os.ModeCharDevice) != 0 +} +