From 456517af873049aee1a50c4e8b7d7a4dfa361942 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Wed, 10 Apr 2024 18:39:08 +0200 Subject: [PATCH] notifier plugin: add support for login succeeded events Signed-off-by: Nicola Murino --- docs/full-configuration.md | 2 +- go.mod | 5 ++--- go.sum | 4 ++++ internal/ftpd/server.go | 4 +++- internal/httpd/api_events.go | 2 ++ internal/httpd/api_utils.go | 4 +++- internal/httpd/internal_test.go | 1 + internal/plugin/notifier.go | 7 +++---- internal/plugin/plugin.go | 30 ++++++++++++++++++++---------- internal/sftpd/server.go | 4 +++- internal/webdavd/server.go | 4 +++- openapi/openapi.yaml | 2 ++ static/locales/en/translation.json | 1 + static/locales/it/translation.json | 1 + templates/webadmin/events.html | 11 +++++++++-- 15 files changed, 58 insertions(+), 24 deletions(-) diff --git a/docs/full-configuration.md b/docs/full-configuration.md index e33f1504..121250cf 100644 --- a/docs/full-configuration.md +++ b/docs/full-configuration.md @@ -482,7 +482,7 @@ The configuration file contains the following sections: - `fs_events`, list of strings. Defines the filesystem events that will be notified to this plugin. - `provider_events`, list of strings. Defines the provider events that will be notified to this plugin. - `provider_objects`, list if strings. Defines the provider objects that will be notified to this plugin. - - `log_events`, list of integers. Defines the log events that will be notified to this plugin. `1` means "Login failed", `2` means "Login with non-existent user", `3` means "No login tried", `4` means "Algorithm negotiation failed". + - `log_events`, list of integers. Defines the log events that will be notified to this plugin. `1` means "Login failed", `2` means "Login with non-existent user", `3` means "No login tried", `4` means "Algorithm negotiation failed", `5` means "Login succeeded". - `retry_max_time`, integer. Defines the maximum number of seconds an event can be late. SFTPGo adds a timestamp to each event and add to an internal queue any events that a the plugin fails to handle (the plugin returns an error or it is not running). If a plugin fails to handle an event that is too late, based on this configuration, it will be discarded. SFTPGo will try to resend queued events every 30 seconds. 0 means no retry. - `retry_queue_max_size`, integer. Defines the maximum number of events that the internal queue can hold. Once the queue is full, the events that cannot be sent to the plugin will be discarded. 0 means no limit. - `kms_options`, struct. Defines the options for kms plugins. diff --git a/go.mod b/go.mod index c54afe79..03633ca8 100644 --- a/go.mod +++ b/go.mod @@ -46,14 +46,14 @@ require ( github.com/minio/sio v0.3.1 github.com/otiai10/copy v1.14.0 github.com/pires/go-proxyproto v0.7.0 - github.com/pkg/sftp v1.13.6 + github.com/pkg/sftp v1.13.7-0.20240410063531-637088883317 github.com/pquerna/otp v1.4.0 github.com/prometheus/client_golang v1.19.0 github.com/robfig/cron/v3 v3.0.1 github.com/rs/cors v1.10.1 github.com/rs/xid v1.5.0 github.com/rs/zerolog v1.32.0 - github.com/sftpgo/sdk v0.1.6-0.20240317102632-f6eb95ea55c3 + github.com/sftpgo/sdk v0.1.6-0.20240409173349-421b3dff3896 github.com/shirou/gopsutil/v3 v3.24.3 github.com/spf13/afero v1.11.0 github.com/spf13/cobra v1.8.0 @@ -185,7 +185,6 @@ require ( replace ( github.com/fclairamb/ftpserverlib => github.com/drakkan/ftpserverlib v0.0.0-20240313174824-cf52df3aa8f7 github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20240210102745-f1ffc43f78d2 - github.com/pkg/sftp => github.com/drakkan/sftp v0.0.0-20240214104840-fbb0b8bdb30c github.com/robfig/cron/v3 => github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0 golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20240405104909-a6b14455cac6 ) diff --git a/go.sum b/go.sum index e005d6c9..3541f63d 100644 --- a/go.sum +++ b/go.sum @@ -314,6 +314,8 @@ github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.7-0.20240410063531-637088883317 h1:kupFhKi4R3XqKmUmqGSHWn/WZbC9CnwSoW421tL1gGw= +github.com/pkg/sftp v1.13.7-0.20240410063531-637088883317/go.mod h1:KMKI0t3T6hfA+lTR/ssZdunHo+uwq7ghoN09/FSu3DY= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -353,6 +355,8 @@ github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sftpgo/sdk v0.1.6-0.20240317102632-f6eb95ea55c3 h1:svxTNm3r2kRlpuVSUKi0WKQlsAq8VI0EzDWPNqeNn/o= github.com/sftpgo/sdk v0.1.6-0.20240317102632-f6eb95ea55c3/go.mod h1:AWoY2YYe/P1ymfTlRER/meERQjCcZZTbgVPGcPQgaqc= +github.com/sftpgo/sdk v0.1.6-0.20240409173349-421b3dff3896 h1:ykxybS9WKurHTatKJ9WjqYD+WH9YH/2QMxCkxUPTVLY= +github.com/sftpgo/sdk v0.1.6-0.20240409173349-421b3dff3896/go.mod h1:AWoY2YYe/P1ymfTlRER/meERQjCcZZTbgVPGcPQgaqc= github.com/shirou/gopsutil/v3 v3.24.3 h1:eoUGJSmdfLzJ3mxIhmOAhgKEKgQkeOwKpz1NbhVnuPE= github.com/shirou/gopsutil/v3 v3.24.3/go.mod h1:JpND7O217xa72ewWz9zN2eIIkPWsDN/3pl0H8Qt0uwg= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= diff --git a/internal/ftpd/server.go b/internal/ftpd/server.go index 9be8711a..adff2631 100644 --- a/internal/ftpd/server.go +++ b/internal/ftpd/server.go @@ -415,7 +415,9 @@ func setStartDirectory(startDirectory string, cc ftpserver.ClientContext) { func updateLoginMetrics(user *dataprovider.User, ip, loginMethod string, err error) { metric.AddLoginAttempt(loginMethod) - if err != nil && err != common.ErrInternalFailure { + if err == nil { + plugin.Handler.NotifyLogEvent(notifier.LogEventTypeLoginOK, common.ProtocolFTP, user.Username, ip, "", nil) + } else if err != common.ErrInternalFailure { logger.ConnectionFailedLog(user.Username, ip, loginMethod, common.ProtocolFTP, err.Error()) event := common.HostEventLoginFailed diff --git a/internal/httpd/api_events.go b/internal/httpd/api_events.go index 37762a07..5c218271 100644 --- a/internal/httpd/api_events.go +++ b/internal/httpd/api_events.go @@ -472,6 +472,8 @@ func getLogEventString(event notifier.LogEventType) string { return "No login tried" case notifier.LogEventTypeNotNegotiated: return "Algorithm negotiation failed" + case notifier.LogEventTypeLoginOK: + return "Login succeeded" default: return "" } diff --git a/internal/httpd/api_utils.go b/internal/httpd/api_utils.go index 56d0f20d..b0fd3eb4 100644 --- a/internal/httpd/api_utils.go +++ b/internal/httpd/api_utils.go @@ -697,7 +697,9 @@ func updateLoginMetrics(user *dataprovider.User, loginMethod, ip string, err err default: protocol = common.ProtocolHTTP } - if err != nil && err != common.ErrInternalFailure && err != common.ErrNoCredentials { + if err == nil { + plugin.Handler.NotifyLogEvent(notifier.LogEventTypeLoginOK, protocol, user.Username, ip, "", nil) + } else if err != common.ErrInternalFailure && err != common.ErrNoCredentials { logger.ConnectionFailedLog(user.Username, ip, loginMethod, protocol, err.Error()) err = handleDefenderEventLoginFailed(ip, err) logEv := notifier.LogEventTypeLoginFailed diff --git a/internal/httpd/internal_test.go b/internal/httpd/internal_test.go index b47b6c91..4bc2399f 100644 --- a/internal/httpd/internal_test.go +++ b/internal/httpd/internal_test.go @@ -3398,6 +3398,7 @@ func TestGetLogEventString(t *testing.T) { assert.Equal(t, "Login with non-existent user", getLogEventString(notifier.LogEventTypeLoginNoUser)) assert.Equal(t, "No login tried", getLogEventString(notifier.LogEventTypeNoLoginTried)) assert.Equal(t, "Algorithm negotiation failed", getLogEventString(notifier.LogEventTypeNotNegotiated)) + assert.Equal(t, "Login succeeded", getLogEventString(notifier.LogEventTypeLoginOK)) assert.Empty(t, getLogEventString(0)) } diff --git a/internal/plugin/notifier.go b/internal/plugin/notifier.go index 12912b07..0e9ac9ec 100644 --- a/internal/plugin/notifier.go +++ b/internal/plugin/notifier.go @@ -44,6 +44,9 @@ func (c *NotifierConfig) hasActions() bool { if len(c.ProviderEvents) > 0 && len(c.ProviderObjects) > 0 { return true } + if len(c.LogEvents) > 0 { + return true + } return false } @@ -250,10 +253,6 @@ func (p *notifierPlugin) notifyProviderAction(event *notifier.ProviderEvent, obj } func (p *notifierPlugin) notifyLogEvent(event *notifier.LogEvent) { - if !util.Contains(p.config.NotifierOptions.LogEvents, int(event.Event)) { - return - } - go func() { Handler.addTask() defer Handler.removeTask() diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go index 6b51b937..91f65a8b 100644 --- a/internal/plugin/plugin.go +++ b/internal/plugin/plugin.go @@ -331,18 +331,28 @@ func (m *Manager) NotifyLogEvent(event notifier.LogEventType, protocol, username m.notifLock.RLock() defer m.notifLock.RUnlock() - e := ¬ifier.LogEvent{ - Timestamp: time.Now().UnixNano(), - Event: event, - Protocol: protocol, - Username: username, - IP: ip, - Message: err.Error(), - Role: role, - } + var e *notifier.LogEvent for _, n := range m.notifiers { - n.notifyLogEvent(e) + if util.Contains(n.config.NotifierOptions.LogEvents, int(event)) { + if e == nil { + message := "" + if err != nil { + message = err.Error() + } + + e = ¬ifier.LogEvent{ + Timestamp: time.Now().UnixNano(), + Event: event, + Protocol: protocol, + Username: username, + IP: ip, + Message: message, + Role: role, + } + } + n.notifyLogEvent(e) + } } } diff --git a/internal/sftpd/server.go b/internal/sftpd/server.go index b6db27e6..8d7861a5 100644 --- a/internal/sftpd/server.go +++ b/internal/sftpd/server.go @@ -1248,7 +1248,9 @@ func (c *Configuration) validateKeyboardInteractiveCredentials(conn ssh.ConnMeta func updateLoginMetrics(user *dataprovider.User, ip, method string, err error) { metric.AddLoginAttempt(method) - if err != nil { + if err == nil { + plugin.Handler.NotifyLogEvent(notifier.LogEventTypeLoginOK, common.ProtocolSSH, user.Username, ip, "", err) + } else { logger.ConnectionFailedLog(user.Username, ip, method, common.ProtocolSSH, err.Error()) if method != dataprovider.SSHLoginMethodPublicKey { // some clients try all available public keys for a user, we diff --git a/internal/webdavd/server.go b/internal/webdavd/server.go index 8a8de264..318b5d86 100644 --- a/internal/webdavd/server.go +++ b/internal/webdavd/server.go @@ -422,7 +422,9 @@ func writeLog(r *http.Request, status int, err error) { func updateLoginMetrics(user *dataprovider.User, ip, loginMethod string, err error) { metric.AddLoginAttempt(loginMethod) - if err != nil && err != common.ErrInternalFailure && err != common.ErrNoCredentials { + if err == nil { + plugin.Handler.NotifyLogEvent(notifier.LogEventTypeLoginOK, common.ProtocolWebDAV, user.Username, ip, "", nil) + } else if err != common.ErrInternalFailure && err != common.ErrNoCredentials { logger.ConnectionFailedLog(user.Username, ip, loginMethod, common.ProtocolWebDAV, err.Error()) event := common.HostEventLoginFailed logEv := notifier.LogEventTypeLoginFailed diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index d14cc8db..3e683555 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -5214,12 +5214,14 @@ components: - 2 - 3 - 4 + - 5 description: > Event status: * `1` - Login failed * `2` - Login failed non-existent user * `3` - No login tried * `4` - Algorithm negotiation failed + * `5` - Login succeeded FsEventStatus: type: integer enum: diff --git a/static/locales/en/translation.json b/static/locales/en/translation.json index b4145112..5d64c84f 100644 --- a/static/locales/en/translation.json +++ b/static/locales/en/translation.json @@ -900,6 +900,7 @@ "add": "Addition", "update": "Update", "login_failed": "Login failed", + "login_ok": "Login succeeded", "login_missing_user": "Login with non-existent user", "no_login_tried": "No login tried", "algo_negotiation_failed": "Algorithm negotiation failed", diff --git a/static/locales/it/translation.json b/static/locales/it/translation.json index ef9ddf34..45904025 100644 --- a/static/locales/it/translation.json +++ b/static/locales/it/translation.json @@ -900,6 +900,7 @@ "add": "Aggiunta", "update": "Aggiornamento", "login_failed": "Accesso fallito", + "login_ok": "Accesso riuscito", "login_missing_user": "Accesso con utente inesistente", "no_login_tried": "Nessun accesso tentato", "algo_negotiation_failed": "Negoziazione algoritmo fallita", diff --git a/templates/webadmin/events.html b/templates/webadmin/events.html index 08ac8c4d..95ae9ace 100644 --- a/templates/webadmin/events.html +++ b/templates/webadmin/events.html @@ -385,6 +385,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com). function selectLogEvents(){ let idActions = $('#idActions'); idActions.empty(); + idActions.append(new Option($.t('events.login_ok'),"5",false,false)); idActions.append(new Option($.t('events.login_failed'),"1",false,false)); idActions.append(new Option($.t('events.login_missing_user'),"2",false,false)); idActions.append(new Option($.t('events.no_login_tried'),"3",false,false)); @@ -875,6 +876,8 @@ explicit grant from the SFTPGo Team (support@sftpgo.com). return $.t('events.no_login_tried'); case 4: return $.t('events.algo_negotiation_failed'); + case 5: + return $.t('events.login_ok'); default: console.log(`unknown log action "${data}"`); return ""; @@ -914,7 +917,9 @@ explicit grant from the SFTPGo Team (support@sftpgo.com). defaultContent: "", render: function(data, type, row) { if (type === 'display') { - return escapeHTML(data); + if (data){ + return escapeHTML(data); + } } return data; } @@ -924,7 +929,9 @@ explicit grant from the SFTPGo Team (support@sftpgo.com). defaultContent: "", render: function(data, type, row) { if (type === 'display') { - return escapeHTML(data); + if (data){ + return escapeHTML(data); + } } return ""; }