From 0fc15dd39805b1f4663ca5b7e392a7551bce8c4a Mon Sep 17 00:00:00 2001 From: Rirush Date: Sun, 3 Jul 2022 18:17:58 +0300 Subject: [PATCH] init: add first version --- README.md | 14 ++++ auth-notify.conf | 2 + auth-notify.service | 11 +++ go.mod | 12 ++++ go.sum | 10 +++ main.go | 172 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 221 insertions(+) create mode 100644 README.md create mode 100644 auth-notify.conf create mode 100644 auth-notify.service create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..60dbaa9 --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# auth-notify + +A small ssh and sudo notification daemon. It parses /var/log/auth.log, searches for successful ssh authentications +and all sudo authentications, and notifies about them both in stdout, and optionally in Telegram. + +Currently only tested with OpenSSH and systemd on Debian. + +## Installation + +Download binary from the release, copy it to `/usr/local/bin/auth-notify`, then copy `auth-notify.conf` into +`/etc/`, and `auth-notify.service` into `/etc/systemd/system/`. + +You can start the service by running `systemctl start auth-notify`, and enable automatic start on boot with +`systemctl enable auth-notify`. \ No newline at end of file diff --git a/auth-notify.conf b/auth-notify.conf new file mode 100644 index 0000000..59bdbe7 --- /dev/null +++ b/auth-notify.conf @@ -0,0 +1,2 @@ +TELEGRAM_CHAT= +TELEGRAM_TOKEN= \ No newline at end of file diff --git a/auth-notify.service b/auth-notify.service new file mode 100644 index 0000000..b74e57a --- /dev/null +++ b/auth-notify.service @@ -0,0 +1,11 @@ +[Unit] +Description=auth-notify is a login notification service +Wants=multi-user.target + +[Service] +ExecStart=/usr/local/bin/auth-notify +EnvironmentFile=/etc/auth-notify.conf +Restart=on-failure + +[Install] +WantedBy=multi-user.target diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..dfb25a9 --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module auth-notify + +go 1.18 + +require github.com/hpcloud/tail v1.0.0 + +require ( + github.com/fsnotify/fsnotify v1.5.4 // indirect + golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect + gopkg.in/fsnotify.v1 v1.4.7 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2527aa7 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= diff --git a/main.go b/main.go new file mode 100644 index 0000000..6f34341 --- /dev/null +++ b/main.go @@ -0,0 +1,172 @@ +package main + +import ( + "encoding/json" + "fmt" + "github.com/hpcloud/tail" + "io" + "log" + "net/http" + "net/url" + "os" + "strings" + "time" +) + +type TelegramResponse struct { + OK bool `json:"ok"` + Description string `json:"description"` +} + +func SendMessage(chat, token, message string) error { + if chat == "" || token == "" { + // If no configuration provided, don't do anything + return nil + } + + endpoint := "https://api.telegram.org/bot" + token + "/sendMessage" + + params := url.Values{} + params.Set("chat_id", chat) + params.Set("text", message) + + c := http.Client{Timeout: 15 * time.Second} + resp, err := c.Get(endpoint + "?" + params.Encode()) + if err != nil { + return err + } + + defer resp.Body.Close() + + data, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + tgResp := &TelegramResponse{} + err = json.Unmarshal(data, tgResp) + + if err != nil { + return err + } + + if !tgResp.OK { + return fmt.Errorf("telegram returned an error: %v", tgResp.Description) + } + + return nil +} + +func main() { + telegramChat := os.Getenv("TELEGRAM_CHAT") + telegramToken := os.Getenv("TELEGRAM_TOKEN") + + logTail, err := tail.TailFile("/var/log/auth.log", tail.Config{ + ReOpen: true, + Follow: true, + }) + if err != nil { + log.Fatalln(err) + } + + skipping := true + now := time.Now() + + for line := range logTail.Lines { + entryFields := strings.SplitN(line.Text, ": ", 2) + if len(entryFields) != 2 { + continue + } + + meta, text := entryFields[0], entryFields[1] + metaFields := strings.Fields(meta) + if len(metaFields) != 5 { + continue + } + + date := strings.Join(metaFields[:3], " ") + hostname := metaFields[3] + unit := metaFields[4] + + t, err := time.Parse("Jan 2 15:04:05", date) + if err != nil { + log.Println("cannot parse time:", err) + continue + } + + // Skip all records that exist before starting the tail + if skipping && (t.Day() != now.Day() || + t.Month() != now.Month() || + t.Hour() < now.Hour() || + (t.Hour() == now.Hour() && t.Minute() < now.Minute()) || + (t.Hour() == now.Hour() && t.Minute() == now.Minute() && t.Second() <= now.Second())) { + continue + } + + skipping = false + + switch { + case strings.HasPrefix(unit, "sshd"): + if !strings.HasPrefix(text, "Accepted") { + continue + } + + sshFields := strings.Fields(text) + if len(sshFields) < 6 { + continue + } + + method, user, ip := sshFields[1], sshFields[3], sshFields[5] + + fmt.Printf("new ssh session for user %v (from %v; using %v)\n", user, ip, method) + err = SendMessage(telegramChat, telegramToken, + fmt.Sprintf("new ssh session started by user %v\n\nfrom: %v\nmethod: %v\nhostname: %v", + user, ip, method, hostname)) + if err != nil { + fmt.Printf("could not send message: %v\n", err) + } + case strings.HasPrefix(unit, "sudo"): + text = strings.TrimSpace(text) + splitSudo := strings.SplitN(text, " : ", 2) + if len(splitSudo) != 2 { + continue + } + + user, rest := splitSudo[0], splitSudo[1] + + failed := strings.Contains(rest, "incorrect password") + + entries := strings.Split(rest, " ; ") + var asUser, command string + + for _, entry := range entries { + switch { + case strings.HasPrefix(entry, "USER="): + asUser = entry[5:] + case strings.HasPrefix(entry, "COMMAND="): + command = entry[8:] + } + } + + if !failed { + fmt.Printf("sudo executed by %v (became %v; for command %v)\n", user, asUser, command) + + err = SendMessage(telegramChat, telegramToken, + fmt.Sprintf("sudo started by %v\n\ntarget: %v\ncommand: %v\nhostname: %v", + user, asUser, command, hostname)) + if err != nil { + fmt.Printf("could not send message: %v\n", err) + } + } else { + fmt.Printf("failed attempt to execute sudo by %v (to become %v; for command %v)\n", user, asUser, command) + + err = SendMessage(telegramChat, telegramToken, + fmt.Sprintf("failed attempt to start sudo by %v\n\ntarget: %v\ncommand: %v\nhostname: %v", + user, asUser, command, hostname)) + if err != nil { + fmt.Printf("could not send message: %v\n", err) + } + } + } + } +}