init v2 project
parent
67c7482abc
commit
cec318a935
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
|
@ -0,0 +1 @@
|
||||||
|
* @sundowndev
|
|
@ -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
|
78
README.md
78
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)
|
[![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?style=flat-square)](https://github.com/sundowndev/covermyass/releases)
|
[![Tag](https://img.shields.io/github/tag/SundownDEV/covermyass.svg)](https://github.com/sundowndev/covermyass/releases)
|
||||||
|
|
||||||
**⚠️ This tool is unmaintained**
|
|
||||||
|
|
||||||
### About
|
### 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.
|
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.
|
||||||
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
With sudo
|
With sudo
|
||||||
|
|
||||||
```bash
|
```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
|
sudo chmod +x /usr/bin/covermyass
|
||||||
```
|
```
|
||||||
|
|
||||||
Without sudo :
|
Without sudo :
|
||||||
|
|
||||||
```bash
|
```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
|
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.
|
||||||
|
|
||||||
Keep in mind that without sudo privileges, you *might* be unable to clear system-level log files (`/var/log`).
|
|
||||||
|
|
||||||
## Usage
|
## 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 !
|
covermyass --write
|
||||||
|
|
||||||
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.*
|
Add custom file paths :
|
||||||
|
|
||||||
Clear logs instantly (requires *sudo* to be efficient) :
|
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo covermyass now
|
covermyass -p '/db/**/*.log'
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using cron job
|
Filter out some paths :
|
||||||
|
|
||||||
Clear bash history every day at 5am :
|
```
|
||||||
|
covermyass -f '/foo/bar/*.log'
|
||||||
```bash
|
covermyass -f '/foo/bar.log'
|
||||||
0 5 * * * covermyass now >/dev/null 2>&1
|
|
||||||
```
|
```
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
|
@ -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())
|
||||||
|
})
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
183
covermyass
183
covermyass
|
@ -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
|
|
|
@ -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
|
||||||
|
)
|
|
@ -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=
|
|
@ -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(),
|
||||||
|
)
|
||||||
|
}
|
|
@ -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])
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package find
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestFinder(t *testing.T) {
|
||||||
|
|
||||||
|
}
|
|
@ -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) {}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package services
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
AddService(NewSSHdService())
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue