notifier plugin: add support for login succeeded events

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2024-04-10 18:39:08 +02:00
parent e8140d7310
commit 456517af87
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
15 changed files with 58 additions and 24 deletions

View file

@ -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. - `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_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. - `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_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. - `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. - `kms_options`, struct. Defines the options for kms plugins.

5
go.mod
View file

@ -46,14 +46,14 @@ require (
github.com/minio/sio v0.3.1 github.com/minio/sio v0.3.1
github.com/otiai10/copy v1.14.0 github.com/otiai10/copy v1.14.0
github.com/pires/go-proxyproto v0.7.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/pquerna/otp v1.4.0
github.com/prometheus/client_golang v1.19.0 github.com/prometheus/client_golang v1.19.0
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/rs/cors v1.10.1 github.com/rs/cors v1.10.1
github.com/rs/xid v1.5.0 github.com/rs/xid v1.5.0
github.com/rs/zerolog v1.32.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/shirou/gopsutil/v3 v3.24.3
github.com/spf13/afero v1.11.0 github.com/spf13/afero v1.11.0
github.com/spf13/cobra v1.8.0 github.com/spf13/cobra v1.8.0
@ -185,7 +185,6 @@ require (
replace ( replace (
github.com/fclairamb/ftpserverlib => github.com/drakkan/ftpserverlib v0.0.0-20240313174824-cf52df3aa8f7 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/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 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 golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20240405104909-a6b14455cac6
) )

4
go.sum
View file

@ -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 h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= 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/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.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 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/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 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.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 h1:eoUGJSmdfLzJ3mxIhmOAhgKEKgQkeOwKpz1NbhVnuPE=
github.com/shirou/gopsutil/v3 v3.24.3/go.mod h1:JpND7O217xa72ewWz9zN2eIIkPWsDN/3pl0H8Qt0uwg= github.com/shirou/gopsutil/v3 v3.24.3/go.mod h1:JpND7O217xa72ewWz9zN2eIIkPWsDN/3pl0H8Qt0uwg=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=

View file

@ -415,7 +415,9 @@ func setStartDirectory(startDirectory string, cc ftpserver.ClientContext) {
func updateLoginMetrics(user *dataprovider.User, ip, loginMethod string, err error) { func updateLoginMetrics(user *dataprovider.User, ip, loginMethod string, err error) {
metric.AddLoginAttempt(loginMethod) 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, logger.ConnectionFailedLog(user.Username, ip, loginMethod,
common.ProtocolFTP, err.Error()) common.ProtocolFTP, err.Error())
event := common.HostEventLoginFailed event := common.HostEventLoginFailed

View file

@ -472,6 +472,8 @@ func getLogEventString(event notifier.LogEventType) string {
return "No login tried" return "No login tried"
case notifier.LogEventTypeNotNegotiated: case notifier.LogEventTypeNotNegotiated:
return "Algorithm negotiation failed" return "Algorithm negotiation failed"
case notifier.LogEventTypeLoginOK:
return "Login succeeded"
default: default:
return "" return ""
} }

View file

@ -697,7 +697,9 @@ func updateLoginMetrics(user *dataprovider.User, loginMethod, ip string, err err
default: default:
protocol = common.ProtocolHTTP 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()) logger.ConnectionFailedLog(user.Username, ip, loginMethod, protocol, err.Error())
err = handleDefenderEventLoginFailed(ip, err) err = handleDefenderEventLoginFailed(ip, err)
logEv := notifier.LogEventTypeLoginFailed logEv := notifier.LogEventTypeLoginFailed

View file

@ -3398,6 +3398,7 @@ func TestGetLogEventString(t *testing.T) {
assert.Equal(t, "Login with non-existent user", getLogEventString(notifier.LogEventTypeLoginNoUser)) assert.Equal(t, "Login with non-existent user", getLogEventString(notifier.LogEventTypeLoginNoUser))
assert.Equal(t, "No login tried", getLogEventString(notifier.LogEventTypeNoLoginTried)) assert.Equal(t, "No login tried", getLogEventString(notifier.LogEventTypeNoLoginTried))
assert.Equal(t, "Algorithm negotiation failed", getLogEventString(notifier.LogEventTypeNotNegotiated)) assert.Equal(t, "Algorithm negotiation failed", getLogEventString(notifier.LogEventTypeNotNegotiated))
assert.Equal(t, "Login succeeded", getLogEventString(notifier.LogEventTypeLoginOK))
assert.Empty(t, getLogEventString(0)) assert.Empty(t, getLogEventString(0))
} }

View file

@ -44,6 +44,9 @@ func (c *NotifierConfig) hasActions() bool {
if len(c.ProviderEvents) > 0 && len(c.ProviderObjects) > 0 { if len(c.ProviderEvents) > 0 && len(c.ProviderObjects) > 0 {
return true return true
} }
if len(c.LogEvents) > 0 {
return true
}
return false return false
} }
@ -250,10 +253,6 @@ func (p *notifierPlugin) notifyProviderAction(event *notifier.ProviderEvent, obj
} }
func (p *notifierPlugin) notifyLogEvent(event *notifier.LogEvent) { func (p *notifierPlugin) notifyLogEvent(event *notifier.LogEvent) {
if !util.Contains(p.config.NotifierOptions.LogEvents, int(event.Event)) {
return
}
go func() { go func() {
Handler.addTask() Handler.addTask()
defer Handler.removeTask() defer Handler.removeTask()

View file

@ -331,18 +331,28 @@ func (m *Manager) NotifyLogEvent(event notifier.LogEventType, protocol, username
m.notifLock.RLock() m.notifLock.RLock()
defer m.notifLock.RUnlock() defer m.notifLock.RUnlock()
e := &notifier.LogEvent{ var e *notifier.LogEvent
Timestamp: time.Now().UnixNano(),
Event: event,
Protocol: protocol,
Username: username,
IP: ip,
Message: err.Error(),
Role: role,
}
for _, n := range m.notifiers { 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 = &notifier.LogEvent{
Timestamp: time.Now().UnixNano(),
Event: event,
Protocol: protocol,
Username: username,
IP: ip,
Message: message,
Role: role,
}
}
n.notifyLogEvent(e)
}
} }
} }

View file

@ -1248,7 +1248,9 @@ func (c *Configuration) validateKeyboardInteractiveCredentials(conn ssh.ConnMeta
func updateLoginMetrics(user *dataprovider.User, ip, method string, err error) { func updateLoginMetrics(user *dataprovider.User, ip, method string, err error) {
metric.AddLoginAttempt(method) 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()) logger.ConnectionFailedLog(user.Username, ip, method, common.ProtocolSSH, err.Error())
if method != dataprovider.SSHLoginMethodPublicKey { if method != dataprovider.SSHLoginMethodPublicKey {
// some clients try all available public keys for a user, we // some clients try all available public keys for a user, we

View file

@ -422,7 +422,9 @@ func writeLog(r *http.Request, status int, err error) {
func updateLoginMetrics(user *dataprovider.User, ip, loginMethod string, err error) { func updateLoginMetrics(user *dataprovider.User, ip, loginMethod string, err error) {
metric.AddLoginAttempt(loginMethod) 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()) logger.ConnectionFailedLog(user.Username, ip, loginMethod, common.ProtocolWebDAV, err.Error())
event := common.HostEventLoginFailed event := common.HostEventLoginFailed
logEv := notifier.LogEventTypeLoginFailed logEv := notifier.LogEventTypeLoginFailed

View file

@ -5214,12 +5214,14 @@ components:
- 2 - 2
- 3 - 3
- 4 - 4
- 5
description: > description: >
Event status: Event status:
* `1` - Login failed * `1` - Login failed
* `2` - Login failed non-existent user * `2` - Login failed non-existent user
* `3` - No login tried * `3` - No login tried
* `4` - Algorithm negotiation failed * `4` - Algorithm negotiation failed
* `5` - Login succeeded
FsEventStatus: FsEventStatus:
type: integer type: integer
enum: enum:

View file

@ -900,6 +900,7 @@
"add": "Addition", "add": "Addition",
"update": "Update", "update": "Update",
"login_failed": "Login failed", "login_failed": "Login failed",
"login_ok": "Login succeeded",
"login_missing_user": "Login with non-existent user", "login_missing_user": "Login with non-existent user",
"no_login_tried": "No login tried", "no_login_tried": "No login tried",
"algo_negotiation_failed": "Algorithm negotiation failed", "algo_negotiation_failed": "Algorithm negotiation failed",

View file

@ -900,6 +900,7 @@
"add": "Aggiunta", "add": "Aggiunta",
"update": "Aggiornamento", "update": "Aggiornamento",
"login_failed": "Accesso fallito", "login_failed": "Accesso fallito",
"login_ok": "Accesso riuscito",
"login_missing_user": "Accesso con utente inesistente", "login_missing_user": "Accesso con utente inesistente",
"no_login_tried": "Nessun accesso tentato", "no_login_tried": "Nessun accesso tentato",
"algo_negotiation_failed": "Negoziazione algoritmo fallita", "algo_negotiation_failed": "Negoziazione algoritmo fallita",

View file

@ -385,6 +385,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
function selectLogEvents(){ function selectLogEvents(){
let idActions = $('#idActions'); let idActions = $('#idActions');
idActions.empty(); 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_failed'),"1",false,false));
idActions.append(new Option($.t('events.login_missing_user'),"2",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)); 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'); return $.t('events.no_login_tried');
case 4: case 4:
return $.t('events.algo_negotiation_failed'); return $.t('events.algo_negotiation_failed');
case 5:
return $.t('events.login_ok');
default: default:
console.log(`unknown log action "${data}"`); console.log(`unknown log action "${data}"`);
return ""; return "";
@ -914,7 +917,9 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
defaultContent: "", defaultContent: "",
render: function(data, type, row) { render: function(data, type, row) {
if (type === 'display') { if (type === 'display') {
return escapeHTML(data); if (data){
return escapeHTML(data);
}
} }
return data; return data;
} }
@ -924,7 +929,9 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
defaultContent: "", defaultContent: "",
render: function(data, type, row) { render: function(data, type, row) {
if (type === 'display') { if (type === 'display') {
return escapeHTML(data); if (data){
return escapeHTML(data);
}
} }
return ""; return "";
} }