diff --git a/README.md b/README.md
index 2fcd4c48..f8b26bb2 100644
--- a/README.md
+++ b/README.md
@@ -48,7 +48,7 @@ You can also purchase support plans from the [SFTPGo website](https://sftpgo.com
SFTPGo is an Open Source project and you can of course use it for free but please don't ask for free support as well.
-We will check the reported issues to see if you are experiencing a bug and if so we'll will fix it, but will only provide support to project [sponsors/donors](#sponsors).
+We will check the reported issues to see if you are experiencing a bug and if so, it may or may not be fixed, we only provide support to project [sponsors/donors](#sponsors).
If you report an invalid issue or ask for step-by-step support, your issue will remain open with no answer or will be closed as invalid without further explanation. Thanks for understanding.
diff --git a/docs/full-configuration.md b/docs/full-configuration.md
index 14c9cc48..0af74fc3 100644
--- a/docs/full-configuration.md
+++ b/docs/full-configuration.md
@@ -467,6 +467,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".
- `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/docs/logs.md b/docs/logs.md
index a1b82080..235a16d7 100644
--- a/docs/logs.md
+++ b/docs/logs.md
@@ -63,5 +63,5 @@ The logs can be divided into the following categories:
- `username`, string. Can be empty if the connection is closed before an authentication attempt
- `client_ip` string.
- `protocol` string. Possible values are `SSH`, `FTP`, `DAV`
- - `login_type` string. Can be `publickey`, `password`, `keyboard-interactive`, `publickey+password`, `publickey+keyboard-interactive` or `no_auth_tryed`
+ - `login_type` string. Can be `publickey`, `password`, `keyboard-interactive`, `publickey+password`, `publickey+keyboard-interactive` or `no_auth_tried`
- `error` string. Optional error description
diff --git a/docs/post-login-hook.md b/docs/post-login-hook.md
index 4ff04f9e..f1a8360b 100644
--- a/docs/post-login-hook.md
+++ b/docs/post-login-hook.md
@@ -8,7 +8,7 @@ If the hook defines an external program it can reads the following environment v
- `SFTPGO_LOGIND_USER`, it contains the user serialized as JSON. The username is empty if the connection is closed for authentication timeout
- `SFTPGO_LOGIND_IP`
-- `SFTPGO_LOGIND_METHOD`, possible values are `publickey`, `password`, `keyboard-interactive`, `publickey+password`, `publickey+keyboard-interactive`, `TLSCertificate`, `TLSCertificate+password` or `no_auth_tryed`, `IDP` (external identity provider)
+- `SFTPGO_LOGIND_METHOD`, possible values are `publickey`, `password`, `keyboard-interactive`, `publickey+password`, `publickey+keyboard-interactive`, `TLSCertificate`, `TLSCertificate+password` or `no_auth_tried`, `IDP` (external identity provider)
- `SFTPGO_LOGIND_STATUS`, 1 means login OK, 0 login KO
- `SFTPGO_LOGIND_PROTOCOL`, possible values are `SSH`, `FTP`, `DAV`, `HTTP`, `OIDC` (OpenID Connect)
diff --git a/go.mod b/go.mod
index 054e9997..299414a6 100644
--- a/go.mod
+++ b/go.mod
@@ -10,14 +10,14 @@ require (
github.com/alexedwards/argon2id v0.0.0-20230305115115-4b3c3280a736
github.com/amoghe/go-crypt v0.0.0-20220222110647-20eada5f5964
github.com/aws/aws-sdk-go-v2 v1.18.0
- github.com/aws/aws-sdk-go-v2/config v1.18.23
- github.com/aws/aws-sdk-go-v2/credentials v1.13.22
+ github.com/aws/aws-sdk-go-v2/config v1.18.25
+ github.com/aws/aws-sdk-go-v2/credentials v1.13.24
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3
- github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.65
+ github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.67
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.14.11
github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.19.7
- github.com/aws/aws-sdk-go-v2/service/sts v1.18.11
+ github.com/aws/aws-sdk-go-v2/service/sts v1.19.0
github.com/bmatcuk/doublestar/v4 v4.6.0
github.com/cockroachdb/cockroach-go/v2 v2.3.3
github.com/coreos/go-oidc/v3 v3.5.0
@@ -53,7 +53,7 @@ require (
github.com/rs/cors v1.9.0
github.com/rs/xid v1.5.0
github.com/rs/zerolog v1.29.1
- github.com/sftpgo/sdk v0.1.3
+ github.com/sftpgo/sdk v0.1.4-0.20230512160325-38e59551f700
github.com/shirou/gopsutil/v3 v3.23.4
github.com/spf13/afero v1.9.5
github.com/spf13/cobra v1.7.0
@@ -68,21 +68,21 @@ require (
go.etcd.io/bbolt v1.3.7
go.uber.org/automaxprocs v1.5.2
gocloud.dev v0.29.0
- golang.org/x/crypto v0.8.0
- golang.org/x/net v0.9.0
- golang.org/x/oauth2 v0.7.0
+ golang.org/x/crypto v0.9.0
+ golang.org/x/net v0.10.0
+ golang.org/x/oauth2 v0.8.0
golang.org/x/sys v0.8.0
golang.org/x/term v0.8.0
golang.org/x/time v0.3.0
- google.golang.org/api v0.121.0
+ google.golang.org/api v0.122.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)
require (
- cloud.google.com/go v0.110.1 // indirect
- cloud.google.com/go/compute v1.19.1 // indirect
+ cloud.google.com/go v0.110.2 // indirect
+ cloud.google.com/go/compute v1.19.2 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
- cloud.google.com/go/iam v1.0.0 // indirect
+ cloud.google.com/go/iam v1.0.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect
github.com/ajg/form v1.5.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
@@ -157,7 +157,7 @@ require (
go.opencensus.io v0.24.0 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/text v0.9.0 // indirect
- golang.org/x/tools v0.8.0 // indirect
+ golang.org/x/tools v0.9.1 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
@@ -170,5 +170,5 @@ require (
replace (
github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
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-20230408075646-704a7f627371
+ golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20230512104844-219592fc3028
)
diff --git a/go.sum b/go.sum
index 1bf2f4f8..7281f60a 100644
--- a/go.sum
+++ b/go.sum
@@ -39,8 +39,8 @@ cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRY
cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM=
cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I=
cloud.google.com/go v0.109.0/go.mod h1:2sYycXt75t/CSB5R9M2wPU1tJmire7AQZTPtITcGBVE=
-cloud.google.com/go v0.110.1 h1:oDJ19Fu9TX9Xs06iyCw4yifSqZ7JQ8BeuVHcTmWQlOA=
-cloud.google.com/go v0.110.1/go.mod h1:uc+V/WjzxQ7vpkxfJhgW4Q4axWXyfAerpQOuSNDZyFw=
+cloud.google.com/go v0.110.2 h1:sdFPBr6xG9/wkBbfhmUz/JmZC7X6LavQgcrVINrKiVA=
+cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw=
cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4=
cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw=
cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o=
@@ -124,8 +124,8 @@ cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARy
cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo=
cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA=
cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs=
-cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY=
-cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE=
+cloud.google.com/go/compute v1.19.2 h1:GbJtPo8OKVHbVep8jvM57KidbYHxeE68LOVqouNLrDY=
+cloud.google.com/go/compute v1.19.2/go.mod h1:5f5a+iC1IriXYauaQ0EyQmEAEq9CGRnV5xJSQSlTV08=
cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU=
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
@@ -218,8 +218,8 @@ cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHD
cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg=
cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE=
cloud.google.com/go/iam v0.10.0/go.mod h1:nXAECrMt2qHpF6RZUZseteD6QyanL68reN4OXPw0UWM=
-cloud.google.com/go/iam v1.0.0 h1:hlQJMovyJJwYjZcTohUH4o1L8Z8kYz+E+W/zktiLCBc=
-cloud.google.com/go/iam v1.0.0/go.mod h1:ikbQ4f1r91wTmBmmOtBCOtuEOei6taatNXytzB7Cxew=
+cloud.google.com/go/iam v1.0.1 h1:lyeCAU6jpnVNrE9zGQkTl3WgNgK/X+uWwaw0kynZJMU=
+cloud.google.com/go/iam v1.0.1/go.mod h1:yR3tmSL8BcZB4bxByRv2jkSIahVmCtfKZwLYGBalRE8=
cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc=
cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A=
cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM=
@@ -565,17 +565,17 @@ github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3eP
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno=
github.com/aws/aws-sdk-go-v2/config v1.18.12/go.mod h1:J36fOhj1LQBr+O4hJCiT8FwVvieeoSGOtPuvhKlsNu8=
-github.com/aws/aws-sdk-go-v2/config v1.18.23 h1:gc3lPsAnZpwfi2exupmgHfva0JiAY2BWDg5JWYlmA28=
-github.com/aws/aws-sdk-go-v2/config v1.18.23/go.mod h1:rx0ruaQ+gk3OrLFHRRx56lA//XxP8K8uPzeNiKNuWVY=
+github.com/aws/aws-sdk-go-v2/config v1.18.25 h1:JuYyZcnMPBiFqn87L2cRppo+rNwgah6YwD3VuyvaW6Q=
+github.com/aws/aws-sdk-go-v2/config v1.18.25/go.mod h1:dZnYpD5wTW/dQF0rRNLVypB396zWCcPiBIvdvSWHEg4=
github.com/aws/aws-sdk-go-v2/credentials v1.13.12/go.mod h1:37HG2MBroXK3jXfxVGtbM2J48ra2+Ltu+tmwr/jO0KA=
-github.com/aws/aws-sdk-go-v2/credentials v1.13.22 h1:Hp9rwJS4giQ48xqonRV/s7QcDf/wxF6UY7osRmBabvI=
-github.com/aws/aws-sdk-go-v2/credentials v1.13.22/go.mod h1:BfNcm6A9nSd+bzejDcMJ5RE+k6WbkCwWkQil7q4heRk=
+github.com/aws/aws-sdk-go-v2/credentials v1.13.24 h1:PjiYyls3QdCrzqUN35jMWtUK1vqVZ+zLfdOa/UPFDp0=
+github.com/aws/aws-sdk-go-v2/credentials v1.13.24/go.mod h1:jYPYi99wUOPIFi0rhiOvXeSEReVOzBqFNOX5bXYoG2o=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.22/go.mod h1:YGSIJyQ6D6FjKMQh16hVFSIUD54L4F7zTGePqYMYYJU=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 h1:jJPgroehGvjrde3XufFIJUZVK5A2L9a3KwSFgKy9n8w=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3/go.mod h1:4Q0UFP0YJf0NrsEuEYHpM9fTSEVnD16Z3uyEF7J9JGM=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.51/go.mod h1:7Grl2gV+dx9SWrUIgwwlUvU40t7+lOSbx34XwfmsTkY=
-github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.65 h1:4irvSxFf0u7pQdtpmUoDSjvMNpOG/8yDUq3orwd9qdg=
-github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.65/go.mod h1:BAWKiL53LT19UMewYr9YhZ8xPO69u6NwmGUjSjRwUdM=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.67 h1:fI9/5BDEaAv/pv1VO1X1n3jfP9it+IGqWsCuuBQI8wM=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.67/go.mod h1:zQClPRIwQZfJlZq6WZve+s4Tb4JW+3V6eS+4+KrYeP8=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.28/go.mod h1:3lwChorpIM/BhImY/hy+Z6jekmN92cXGPI1QJasVPYY=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 h1:kG5eQilShqmJbv11XL1VpyDbaEJzWxd4zRiCG30GSn4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33/go.mod h1:7i0PF1ME/2eUPFcjkVIwq+DOygHEoK92t5cDqNgYbIw=
@@ -619,8 +619,8 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.1/go.mod h1:O1YSOg3aekZibh2Sn
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10 h1:PkHIIJs8qvq0e5QybnZoG1K/9QTrLr9OsqCIo59jOBA=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10/go.mod h1:AFvkxc8xfBe8XA+5St5XIHHrQQtkxqrRincx4hmMHOk=
github.com/aws/aws-sdk-go-v2/service/sts v1.18.3/go.mod h1:b+psTJn33Q4qGoDaM7ZiOVVG8uVjGI6HaZ8WBHdgDgU=
-github.com/aws/aws-sdk-go-v2/service/sts v1.18.11 h1:uBE+Zj478pfxV98L6SEpvxYiADNjTlMNY714PJLE7uo=
-github.com/aws/aws-sdk-go-v2/service/sts v1.18.11/go.mod h1:BgQOMsg8av8jset59jelyPW7NoZcZXLVpDsXunGDrk8=
+github.com/aws/aws-sdk-go-v2/service/sts v1.19.0 h1:2DQLAKDteoEDI8zpCzqBMaZlJuoE9iTYD0gFmXVax9E=
+github.com/aws/aws-sdk-go-v2/service/sts v1.19.0/go.mod h1:BgQOMsg8av8jset59jelyPW7NoZcZXLVpDsXunGDrk8=
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
@@ -877,8 +877,8 @@ github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZ
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0 h1:EW9gIJRmt9lzk66Fhh4S8VEtURA6QHZqGeSRE9Nb2/U=
github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
-github.com/drakkan/crypto v0.0.0-20230408075646-704a7f627371 h1:e2fWtTFAkFfNOeqww6HsEhtxETjGUBKnmIbMNB7V8mg=
-github.com/drakkan/crypto v0.0.0-20230408075646-704a7f627371/go.mod h1:svd5Kbdx1UEmxh6mV0H38ASBeI90vEuujcyP74bw210=
+github.com/drakkan/crypto v0.0.0-20230512104844-219592fc3028 h1:qUrs/afB0gubJUY5kOmxLx1euFlXn9yUMUhli7Njob8=
+github.com/drakkan/crypto v0.0.0-20230512104844-219592fc3028/go.mod h1:FPowDKc1rEQhN3Xf48AhpBr8eSNzpEYaAQczEYcuAVU=
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 h1:LPH1dEblAOO/LoG7yHPMtBLXhQmjaga91/DDjWk9jWA=
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU=
github.com/drakkan/webdav v0.0.0-20230227175313-32996838bcd8 h1:tdkLkSKtYd3WSDsZXGJDKsakiNstLQJPN5HjnqCkf2c=
@@ -1842,8 +1842,8 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
-github.com/sftpgo/sdk v0.1.3 h1:o/9herRbrDH6sQwfpKlV3AV0R7qJgOe/x4yQnEIWIHk=
-github.com/sftpgo/sdk v0.1.3/go.mod h1:gDxDaU3rhp9Y92ddsE7SbQ8jdBNNWK1DKlp5eHXrsb8=
+github.com/sftpgo/sdk v0.1.4-0.20230512160325-38e59551f700 h1:jL6mfKAaFv862AnBUxIfTH9wmnuPjbWyjHQUGDo+Xt0=
+github.com/sftpgo/sdk v0.1.4-0.20230512160325-38e59551f700/go.mod h1:gDxDaU3rhp9Y92ddsE7SbQ8jdBNNWK1DKlp5eHXrsb8=
github.com/shirou/gopsutil/v3 v3.23.4 h1:hZwmDxZs7Ewt75DV81r4pFMqbq+di2cbt9FsQBqLD2o=
github.com/shirou/gopsutil/v3 v3.23.4/go.mod h1:ZcGxyfzAMRevhUR2+cfhXDH6gQdFYE/t8j1nsU4mPI8=
github.com/shoenig/go-m1cpu v0.1.5/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ=
@@ -2250,8 +2250,8 @@ golang.org/x/net v0.3.1-0.20221206200815-1e63c2f08a10/go.mod h1:MBQ8lrhLObU/6UmL
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
-golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
+golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -2283,8 +2283,8 @@ golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri
golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk=
golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec=
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
-golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
-golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
+golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
+golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -2301,8 +2301,8 @@ golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -2468,7 +2468,6 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -2590,8 +2589,8 @@ golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
-golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
-golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
+golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
+golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -2668,8 +2667,8 @@ google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/
google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY=
google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY=
google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI=
-google.golang.org/api v0.121.0 h1:8Oopoo8Vavxx6gt+sgs8s8/X60WBAtKQq6JqnkF+xow=
-google.golang.org/api v0.121.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms=
+google.golang.org/api v0.122.0 h1:zDobeejm3E7pEG1mNHvdxvjs5XJoCMzyNH+CmwL94Es=
+google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
diff --git a/internal/common/common.go b/internal/common/common.go
index 296ba3ba..b9c9794a 100644
--- a/internal/common/common.go
+++ b/internal/common/common.go
@@ -32,6 +32,7 @@ import (
"time"
"github.com/pires/go-proxyproto"
+ "github.com/sftpgo/sdk/plugin/notifier"
"github.com/drakkan/sftpgo/v2/internal/command"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
@@ -965,12 +966,14 @@ func (conns *ActiveConnections) Remove(connectionID string) {
conn.GetLocalAddress(), conn.GetRemoteAddress(), err, lastIdx)
if conn.GetProtocol() == ProtocolFTP && conn.GetUsername() == "" && !util.Contains(ftpLoginCommands, conn.GetCommand()) {
ip := util.GetIPFromRemoteAddress(conn.GetRemoteAddress())
- logger.ConnectionFailedLog("", ip, dataprovider.LoginMethodNoAuthTryed, conn.GetProtocol(),
- dataprovider.ErrNoAuthTryed.Error())
- metric.AddNoAuthTryed()
+ logger.ConnectionFailedLog("", ip, dataprovider.LoginMethodNoAuthTried, ProtocolFTP,
+ dataprovider.ErrNoAuthTried.Error())
+ metric.AddNoAuthTried()
AddDefenderEvent(ip, ProtocolFTP, HostEventNoLoginTried)
- dataprovider.ExecutePostLoginHook(&dataprovider.User{}, dataprovider.LoginMethodNoAuthTryed, ip,
- conn.GetProtocol(), dataprovider.ErrNoAuthTryed)
+ dataprovider.ExecutePostLoginHook(&dataprovider.User{}, dataprovider.LoginMethodNoAuthTried, ip,
+ ProtocolFTP, dataprovider.ErrNoAuthTried)
+ plugin.Handler.NotifyLogEvent(notifier.LogEventTypeNoLoginTried, ProtocolFTP, "", ip, "",
+ dataprovider.ErrNoAuthTried)
}
Config.checkPostDisconnectHook(conn.GetRemoteAddress(), conn.GetProtocol(), conn.GetUsername(),
conn.GetID(), conn.GetConnectionTime())
diff --git a/internal/common/dataretention.go b/internal/common/dataretention.go
index 28787047..d3faf890 100644
--- a/internal/common/dataretention.go
+++ b/internal/common/dataretention.go
@@ -325,7 +325,7 @@ func (c *RetentionCheck) checkEmptyDirRemoval(folderPath string) {
files, err := c.conn.ListDir(folderPath)
if err == nil && len(files) == 0 {
err = c.conn.RemoveDir(folderPath)
- c.conn.Log(logger.LevelDebug, "tryed to remove empty dir %q, error: %v", folderPath, err)
+ c.conn.Log(logger.LevelDebug, "tried to remove empty dir %q, error: %v", folderPath, err)
}
}
}
diff --git a/internal/config/config.go b/internal/config/config.go
index 54db833f..6d3a61e0 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -964,6 +964,21 @@ func getNotifierPluginFromEnv(idx int, pluginConfig *plugin.Config) bool {
isSet = true
}
+ notifierLogEventsString, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__NOTIFIER_OPTIONS__LOG_EVENTS", idx))
+ if ok {
+ var notifierLogEvents []int
+ for _, e := range notifierLogEventsString {
+ ev, err := strconv.Atoi(e)
+ if err == nil {
+ notifierLogEvents = append(notifierLogEvents, ev)
+ }
+ }
+ if len(notifierLogEvents) > 0 {
+ pluginConfig.NotifierOptions.LogEvents = notifierLogEvents
+ isSet = true
+ }
+ }
+
notifierRetryMaxTime, ok := lookupIntFromEnv(fmt.Sprintf("SFTPGO_PLUGINS__%v__NOTIFIER_OPTIONS__RETRY_MAX_TIME", idx), 0)
if ok {
pluginConfig.NotifierOptions.RetryMaxTime = int(notifierRetryMaxTime)
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 939934b3..2a706c55 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -698,6 +698,7 @@ func TestPluginsFromEnv(t *testing.T) {
os.Setenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__FS_EVENTS", "upload,download")
os.Setenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__PROVIDER_EVENTS", "add,update")
os.Setenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__PROVIDER_OBJECTS", "user,admin")
+ os.Setenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__LOG_EVENTS", "a,1,2")
os.Setenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__RETRY_MAX_TIME", "2")
os.Setenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__RETRY_QUEUE_MAX_SIZE", "1000")
os.Setenv("SFTPGO_PLUGINS__0__CMD", "plugin_start_cmd")
@@ -712,6 +713,7 @@ func TestPluginsFromEnv(t *testing.T) {
os.Unsetenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__FS_EVENTS")
os.Unsetenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__PROVIDER_EVENTS")
os.Unsetenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__PROVIDER_OBJECTS")
+ os.Unsetenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__LOG_EVENTS")
os.Unsetenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__RETRY_MAX_TIME")
os.Unsetenv("SFTPGO_PLUGINS__0__NOTIFIER_OPTIONS__RETRY_QUEUE_MAX_SIZE")
os.Unsetenv("SFTPGO_PLUGINS__0__CMD")
@@ -738,6 +740,9 @@ func TestPluginsFromEnv(t *testing.T) {
require.Len(t, pluginConf.NotifierOptions.ProviderObjects, 2)
require.Equal(t, "user", pluginConf.NotifierOptions.ProviderObjects[0])
require.Equal(t, "admin", pluginConf.NotifierOptions.ProviderObjects[1])
+ require.Len(t, pluginConf.NotifierOptions.LogEvents, 2)
+ require.Equal(t, 1, pluginConf.NotifierOptions.LogEvents[0])
+ require.Equal(t, 2, pluginConf.NotifierOptions.LogEvents[1])
require.Equal(t, 2, pluginConf.NotifierOptions.RetryMaxTime)
require.Equal(t, 1000, pluginConf.NotifierOptions.RetryQueueMaxSize)
require.Equal(t, "plugin_start_cmd", pluginConf.Cmd)
diff --git a/internal/dataprovider/dataprovider.go b/internal/dataprovider/dataprovider.go
index 4da37155..6edd43b9 100644
--- a/internal/dataprovider/dataprovider.go
+++ b/internal/dataprovider/dataprovider.go
@@ -159,8 +159,8 @@ var (
LoginMethodTLSCertificate, LoginMethodTLSCertificateAndPwd}
// SSHMultiStepsLoginMethods defines the supported Multi-Step Authentications
SSHMultiStepsLoginMethods = []string{SSHLoginMethodKeyAndPassword, SSHLoginMethodKeyAndKeyboardInt}
- // ErrNoAuthTryed defines the error for connection closed before authentication
- ErrNoAuthTryed = errors.New("no auth tryed")
+ // ErrNoAuthTried defines the error for connection closed before authentication
+ ErrNoAuthTried = errors.New("no auth tried")
// ErrNotImplemented defines the error for features not supported for a particular data provider
ErrNotImplemented = errors.New("feature not supported with the configured data provider")
// ValidProtocols defines all the valid protcols
diff --git a/internal/dataprovider/user.go b/internal/dataprovider/user.go
index 1793ae92..9cb020f3 100644
--- a/internal/dataprovider/user.go
+++ b/internal/dataprovider/user.go
@@ -76,7 +76,7 @@ const (
// Available login methods
const (
- LoginMethodNoAuthTryed = "no_auth_tryed"
+ LoginMethodNoAuthTried = "no_auth_tried"
LoginMethodPassword = "password"
SSHLoginMethodPassword = "password-over-SSH"
SSHLoginMethodPublicKey = "publickey"
diff --git a/internal/ftpd/server.go b/internal/ftpd/server.go
index 460d4dc3..c9715d0c 100644
--- a/internal/ftpd/server.go
+++ b/internal/ftpd/server.go
@@ -25,11 +25,13 @@ import (
"sync"
ftpserver "github.com/fclairamb/ftpserverlib"
+ "github.com/sftpgo/sdk/plugin/notifier"
"github.com/drakkan/sftpgo/v2/internal/common"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
"github.com/drakkan/sftpgo/v2/internal/logger"
"github.com/drakkan/sftpgo/v2/internal/metric"
+ "github.com/drakkan/sftpgo/v2/internal/plugin"
"github.com/drakkan/sftpgo/v2/internal/util"
"github.com/drakkan/sftpgo/v2/internal/version"
)
@@ -426,10 +428,13 @@ func updateLoginMetrics(user *dataprovider.User, ip, loginMethod string, err err
logger.ConnectionFailedLog(user.Username, ip, loginMethod,
common.ProtocolFTP, err.Error())
event := common.HostEventLoginFailed
+ logEv := notifier.LogEventTypeLoginFailed
if errors.Is(err, util.ErrNotFound) {
event = common.HostEventUserNotFound
+ logEv = notifier.LogEventTypeLoginNoUser
}
common.AddDefenderEvent(ip, common.ProtocolFTP, event)
+ plugin.Handler.NotifyLogEvent(logEv, common.ProtocolFTP, user.Username, ip, "", err)
}
metric.AddLoginResult(loginMethod, err)
dataprovider.ExecutePostLoginHook(user, loginMethod, ip, common.ProtocolFTP, err)
diff --git a/internal/httpd/api_events.go b/internal/httpd/api_events.go
index 408dfb8f..f9fcc445 100644
--- a/internal/httpd/api_events.go
+++ b/internal/httpd/api_events.go
@@ -24,6 +24,7 @@ import (
"time"
"github.com/sftpgo/sdk/plugin/eventsearcher"
+ "github.com/sftpgo/sdk/plugin/notifier"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
"github.com/drakkan/sftpgo/v2/internal/plugin"
@@ -67,11 +68,10 @@ func getCommonSearchParamsFromRequest(r *http.Request) (eventsearcher.CommonSear
}
c.EndTimestamp = ts
}
- c.Actions = getCommaSeparatedQueryParam(r, "actions")
c.Username = r.URL.Query().Get("username")
c.IP = r.URL.Query().Get("ip")
c.InstanceIDs = getCommaSeparatedQueryParam(r, "instance_ids")
- c.ExcludeIDs = getCommaSeparatedQueryParam(r, "exclude_ids")
+ c.FromID = r.URL.Query().Get("from_id")
return c, nil
}
@@ -92,6 +92,7 @@ func getFsSearchParamsFromRequest(r *http.Request) (eventsearcher.FsEventSearch,
}
s.FsProvider = val
}
+ s.Actions = getCommaSeparatedQueryParam(r, "actions")
s.SSHCmd = r.URL.Query().Get("ssh_cmd")
s.Bucket = r.URL.Query().Get("bucket")
s.Endpoint = r.URL.Query().Get("endpoint")
@@ -115,11 +116,31 @@ func getProviderSearchParamsFromRequest(r *http.Request) (eventsearcher.Provider
if err != nil {
return s, err
}
+ s.Actions = getCommaSeparatedQueryParam(r, "actions")
s.ObjectName = r.URL.Query().Get("object_name")
s.ObjectTypes = getCommaSeparatedQueryParam(r, "object_types")
return s, nil
}
+func getLogSearchParamsFromRequest(r *http.Request) (eventsearcher.LogEventSearch, error) {
+ var err error
+ s := eventsearcher.LogEventSearch{}
+ s.CommonSearchParams, err = getCommonSearchParamsFromRequest(r)
+ if err != nil {
+ return s, err
+ }
+ s.Protocols = getCommaSeparatedQueryParam(r, "protocols")
+ events := getCommaSeparatedQueryParam(r, "events")
+ for _, ev := range events {
+ evType, err := strconv.ParseUint(ev, 10, 32)
+ if err == nil {
+ s.Events = append(s.Events, int32(evType))
+ }
+ }
+
+ return s, nil
+}
+
func searchFsEvents(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
claims, err := getTokenClaims(r)
@@ -143,7 +164,7 @@ func searchFsEvents(w http.ResponseWriter, r *http.Request) {
return
}
- data, _, _, err := plugin.Handler.SearchFsEvents(&filters)
+ data, err := plugin.Handler.SearchFsEvents(&filters)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
@@ -178,7 +199,40 @@ func searchProviderEvents(w http.ResponseWriter, r *http.Request) {
return
}
- data, _, _, err := plugin.Handler.SearchProviderEvents(&filters)
+ data, err := plugin.Handler.SearchProviderEvents(&filters)
+ if err != nil {
+ sendAPIResponse(w, r, err, "", getRespStatus(err))
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ w.Write(data) //nolint:errcheck
+}
+
+func searchLogEvents(w http.ResponseWriter, r *http.Request) {
+ r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
+ claims, err := getTokenClaims(r)
+ if err != nil || claims.Username == "" {
+ sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
+ return
+ }
+
+ var filters eventsearcher.LogEventSearch
+ if filters, err = getLogSearchParamsFromRequest(r); err != nil {
+ sendAPIResponse(w, r, err, "", getRespStatus(err))
+ return
+ }
+ filters.Role = getRoleFilterForEventSearch(r, claims.Role)
+
+ if getBoolQueryParam(r, "csv_export") {
+ filters.Limit = 100
+ if err := exportLogEvents(w, &filters); err != nil {
+ panic(http.ErrAbortHandler)
+ }
+ return
+ }
+
+ data, err := plugin.Handler.SearchLogEvents(&filters)
if err != nil {
sendAPIResponse(w, r, err, "", getRespStatus(err))
return
@@ -202,7 +256,7 @@ func exportFsEvents(w http.ResponseWriter, filters *eventsearcher.FsEventSearch)
}
results := make([]fsEvent, 0, filters.Limit)
for {
- data, _, _, err := plugin.Handler.SearchFsEvents(filters)
+ data, err := plugin.Handler.SearchFsEvents(filters)
if err != nil {
return err
}
@@ -218,7 +272,7 @@ func exportFsEvents(w http.ResponseWriter, filters *eventsearcher.FsEventSearch)
break
}
filters.StartTimestamp = results[len(results)-1].Timestamp
- filters.ExcludeIDs = []string{results[len(results)-1].ID}
+ filters.FromID = results[len(results)-1].ID
results = nil
}
csvWriter.Flush()
@@ -239,7 +293,44 @@ func exportProviderEvents(w http.ResponseWriter, filters *eventsearcher.Provider
}
results := make([]providerEvent, 0, filters.Limit)
for {
- data, _, _, err := plugin.Handler.SearchProviderEvents(filters)
+ data, err := plugin.Handler.SearchProviderEvents(filters)
+ if err != nil {
+ return err
+ }
+ if err := json.Unmarshal(data, &results); err != nil {
+ return err
+ }
+ for _, event := range results {
+ if err := csvWriter.Write(event.getCSVData()); err != nil {
+ return err
+ }
+ }
+ if len(results) < filters.Limit || len(results) == 0 {
+ break
+ }
+ filters.FromID = results[len(results)-1].ID
+ filters.StartTimestamp = results[len(results)-1].Timestamp
+ results = nil
+ }
+ csvWriter.Flush()
+ return csvWriter.Error()
+}
+
+func exportLogEvents(w http.ResponseWriter, filters *eventsearcher.LogEventSearch) error {
+ w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=logs-%s.csv", time.Now().Format("2006-01-02T15-04-05")))
+ w.Header().Set("Content-Type", "text/csv")
+ w.Header().Set("Accept-Ranges", "none")
+ w.WriteHeader(http.StatusOK)
+
+ ev := logEvent{}
+ csvWriter := csv.NewWriter(w)
+ err := csvWriter.Write(ev.getCSVHeader())
+ if err != nil {
+ return err
+ }
+ results := make([]logEvent, 0, filters.Limit)
+ for {
+ data, err := plugin.Handler.SearchLogEvents(filters)
if err != nil {
return err
}
@@ -255,7 +346,7 @@ func exportProviderEvents(w http.ResponseWriter, filters *eventsearcher.Provider
break
}
filters.StartTimestamp = results[len(results)-1].Timestamp
- filters.ExcludeIDs = []string{results[len(results)-1].ID}
+ filters.FromID = results[len(results)-1].ID
results = nil
}
csvWriter.Flush()
@@ -349,3 +440,39 @@ func (e *providerEvent) getCSVData() []string {
return []string{timestamp.Format(time.RFC3339Nano), e.Action, e.ObjectType, e.ObjectName,
e.Username, e.IP}
}
+
+type logEvent struct {
+ ID string `json:"id"`
+ Timestamp int64 `json:"timestamp"`
+ Event int `json:"event"`
+ Protocol string `json:"protocol"`
+ Username string `json:"username,omitempty"`
+ IP string `json:"ip,omitempty"`
+ Message string `json:"message,omitempty"`
+ Role string `json:"role,omitempty"`
+}
+
+func (e *logEvent) getCSVHeader() []string {
+ return []string{"Time", "Event", "Protocol", "User", "IP", "Message"}
+}
+
+func (e *logEvent) getCSVData() []string {
+ timestamp := time.Unix(0, e.Timestamp).UTC()
+ return []string{timestamp.Format(time.RFC3339Nano), getLogEventString(notifier.LogEventType(e.Event)),
+ e.Protocol, e.Username, e.IP, e.Message}
+}
+
+func getLogEventString(event notifier.LogEventType) string {
+ switch event {
+ case notifier.LogEventTypeLoginFailed:
+ return "Login failed"
+ case notifier.LogEventTypeLoginNoUser:
+ return "Login with non-existent user"
+ case notifier.LogEventTypeNoLoginTried:
+ return "No login tried"
+ case notifier.LogEventTypeNotNegotiated:
+ return "Algorithm negotiation failed"
+ default:
+ return ""
+ }
+}
diff --git a/internal/httpd/api_utils.go b/internal/httpd/api_utils.go
index 349c86d4..289ac77b 100644
--- a/internal/httpd/api_utils.go
+++ b/internal/httpd/api_utils.go
@@ -35,6 +35,7 @@ import (
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/render"
"github.com/klauspost/compress/zip"
+ "github.com/sftpgo/sdk/plugin/notifier"
"github.com/drakkan/sftpgo/v2/internal/common"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
@@ -614,6 +615,11 @@ func updateLoginMetrics(user *dataprovider.User, loginMethod, ip string, err err
if err != nil && err != common.ErrInternalFailure && err != common.ErrNoCredentials {
logger.ConnectionFailedLog(user.Username, ip, loginMethod, protocol, err.Error())
err = handleDefenderEventLoginFailed(ip, err)
+ logEv := notifier.LogEventTypeLoginFailed
+ if errors.Is(err, util.ErrNotFound) {
+ logEv = notifier.LogEventTypeLoginNoUser
+ }
+ plugin.Handler.NotifyLogEvent(logEv, protocol, user.Username, ip, "", err)
}
metric.AddLoginResult(loginMethod, err)
dataprovider.ExecutePostLoginHook(user, loginMethod, ip, protocol, err)
diff --git a/internal/httpd/httpd.go b/internal/httpd/httpd.go
index f5442558..69dac543 100644
--- a/internal/httpd/httpd.go
+++ b/internal/httpd/httpd.go
@@ -91,6 +91,7 @@ const (
metadataChecksPath = "/api/v2/metadata/users/checks"
fsEventsPath = "/api/v2/events/fs"
providerEventsPath = "/api/v2/events/provider"
+ logEventsPath = "/api/v2/events/logs"
sharesPath = "/api/v2/shares"
eventActionsPath = "/api/v2/eventactions"
eventRulesPath = "/api/v2/eventrules"
@@ -148,6 +149,7 @@ const (
webEventsPathDefault = "/web/admin/events"
webEventsFsSearchPathDefault = "/web/admin/events/fs"
webEventsProviderSearchPathDefault = "/web/admin/events/provider"
+ webEventsLogSearchPathDefault = "/web/admin/events/logs"
webConfigsPathDefault = "/web/admin/configs"
webClientLoginPathDefault = "/web/client/login"
webClientOIDCLoginPathDefault = "/web/client/oidclogin"
@@ -243,6 +245,7 @@ var (
webEventsPath string
webEventsFsSearchPath string
webEventsProviderSearchPath string
+ webEventsLogSearchPath string
webConfigsPath string
webDefenderHostsPath string
webClientLoginPath string
@@ -1142,6 +1145,7 @@ func updateWebAdminURLs(baseURL string) {
webEventsPath = path.Join(baseURL, webEventsPathDefault)
webEventsFsSearchPath = path.Join(baseURL, webEventsFsSearchPathDefault)
webEventsProviderSearchPath = path.Join(baseURL, webEventsProviderSearchPathDefault)
+ webEventsLogSearchPath = path.Join(baseURL, webEventsLogSearchPathDefault)
webConfigsPath = path.Join(baseURL, webConfigsPathDefault)
webStaticFilesPath = path.Join(baseURL, webStaticFilesPathDefault)
webOpenAPIPath = path.Join(baseURL, webOpenAPIPathDefault)
diff --git a/internal/httpd/httpd_test.go b/internal/httpd/httpd_test.go
index df1e140c..606091d6 100644
--- a/internal/httpd/httpd_test.go
+++ b/internal/httpd/httpd_test.go
@@ -125,6 +125,7 @@ const (
metadataBasePath = "/api/v2/metadata/users"
fsEventsPath = "/api/v2/events/fs"
providerEventsPath = "/api/v2/events/provider"
+ logEventsPath = "/api/v2/events/logs"
sharesPath = "/api/v2/shares"
eventActionsPath = "/api/v2/eventactions"
eventRulesPath = "/api/v2/eventrules"
@@ -9869,12 +9870,65 @@ func TestSearchEvents(t *testing.T) {
}
exportFunc()
+ req, err = http.NewRequest(http.MethodGet, logEventsPath, nil)
+ assert.NoError(t, err)
+ setBearerForReq(req, token)
+ rr = executeRequest(req)
+ checkResponseCode(t, http.StatusOK, rr)
+ events = make([]map[string]any, 0)
+ err = json.Unmarshal(rr.Body.Bytes(), &events)
+ assert.NoError(t, err)
+ if assert.Len(t, events, 1) {
+ ev := events[0]
+ for _, field := range []string{"id", "timestamp", "event", "ip", "message", "role", "instance_id"} {
+ _, ok := ev[field]
+ assert.True(t, ok, field)
+ }
+ }
+ req, err = http.NewRequest(http.MethodGet, logEventsPath+"?events=a,1", nil)
+ assert.NoError(t, err)
+ setBearerForReq(req, token)
+ rr = executeRequest(req)
+ checkResponseCode(t, http.StatusOK, rr)
+ // CSV export
+ req, err = http.NewRequest(http.MethodGet, logEventsPath+"?csv_export=true", nil)
+ assert.NoError(t, err)
+ setBearerForReq(req, token)
+ rr = executeRequest(req)
+ checkResponseCode(t, http.StatusOK, rr)
+ assert.Equal(t, "text/csv", rr.Header().Get("Content-Type"))
+ // the test eventsearcher plugin returns error if start_timestamp < 0
+ req, err = http.NewRequest(http.MethodGet, logEventsPath+"?start_timestamp=-1", nil)
+ assert.NoError(t, err)
+ setBearerForReq(req, token)
+ rr = executeRequest(req)
+ checkResponseCode(t, http.StatusInternalServerError, rr)
+ // CSV export with error
+ exportFunc = func() {
+ defer func() {
+ rcv := recover()
+ assert.Equal(t, http.ErrAbortHandler, rcv)
+ }()
+
+ req, err = http.NewRequest(http.MethodGet, logEventsPath+"?start_timestamp=-1&csv_export=true", nil)
+ assert.NoError(t, err)
+ setBearerForReq(req, token)
+ rr = executeRequest(req)
+ }
+ exportFunc()
+
req, err = http.NewRequest(http.MethodGet, providerEventsPath+"?limit=2000", nil)
assert.NoError(t, err)
setBearerForReq(req, token)
rr = executeRequest(req)
checkResponseCode(t, http.StatusBadRequest, rr)
+ req, err = http.NewRequest(http.MethodGet, logEventsPath+"?limit=2000", nil)
+ assert.NoError(t, err)
+ setBearerForReq(req, token)
+ rr = executeRequest(req)
+ checkResponseCode(t, http.StatusBadRequest, rr)
+
req, err = http.NewRequest(http.MethodGet, fsEventsPath+"?start_timestamp=a", nil)
assert.NoError(t, err)
setBearerForReq(req, token)
diff --git a/internal/httpd/internal_test.go b/internal/httpd/internal_test.go
index 72f2c57e..ed2034c6 100644
--- a/internal/httpd/internal_test.go
+++ b/internal/httpd/internal_test.go
@@ -44,6 +44,7 @@ import (
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/rs/xid"
"github.com/sftpgo/sdk"
+ "github.com/sftpgo/sdk/plugin/notifier"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -785,6 +786,11 @@ func TestInvalidToken(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid token claims")
+ rr = httptest.NewRecorder()
+ searchLogEvents(rr, req)
+ assert.Equal(t, http.StatusBadRequest, rr.Code)
+ assert.Contains(t, rr.Body.String(), "Invalid token claims")
+
rr = httptest.NewRecorder()
addIPListEntry(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
@@ -3224,6 +3230,14 @@ func TestHTTPSRedirect(t *testing.T) {
assert.NoError(t, err)
}
+func TestGetLogEventString(t *testing.T) {
+ assert.Equal(t, "Login failed", getLogEventString(notifier.LogEventTypeLoginFailed))
+ 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.Empty(t, getLogEventString(0))
+}
+
func isSharedProviderSupported() bool {
// SQLite shares the implementation with other SQL-based provider but it makes no sense
// to use it outside test cases
diff --git a/internal/httpd/server.go b/internal/httpd/server.go
index cd0d1a34..c1e4f3d7 100644
--- a/internal/httpd/server.go
+++ b/internal/httpd/server.go
@@ -1322,6 +1322,8 @@ func (s *httpdServer) initializeRouter() {
Get(fsEventsPath, searchFsEvents)
router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler).
Get(providerEventsPath, searchProviderEvents)
+ router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler).
+ Get(logEventsPath, searchLogEvents)
router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)).
Get(apiKeysPath, getAPIKeys)
router.With(forbidAPIKeyAuthentication, s.checkPerm(dataprovider.PermAdminManageAPIKeys)).
@@ -1724,6 +1726,8 @@ func (s *httpdServer) setupWebAdminRoutes() {
Get(webEventsFsSearchPath, searchFsEvents)
router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler, s.refreshCookie).
Get(webEventsProviderSearchPath, searchProviderEvents)
+ router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler, s.refreshCookie).
+ Get(webEventsLogSearchPath, searchLogEvents)
router.With(s.checkPerm(dataprovider.PermAdminManageIPLists)).Get(webIPListsPath, s.handleWebIPListsPage)
router.With(s.checkPerm(dataprovider.PermAdminManageIPLists), compressor.Handler, s.refreshCookie).
Get(webIPListsPath+"/{type}", getIPListEntries)
diff --git a/internal/httpd/webadmin.go b/internal/httpd/webadmin.go
index 92438c90..022ab8b9 100644
--- a/internal/httpd/webadmin.go
+++ b/internal/httpd/webadmin.go
@@ -388,6 +388,7 @@ type eventsPage struct {
basePage
FsEventsSearchURL string
ProviderEventsSearchURL string
+ LogEventsSearchURL string
}
type configsPage struct {
@@ -3944,6 +3945,7 @@ func (s *httpdServer) handleWebGetEvents(w http.ResponseWriter, r *http.Request)
basePage: s.getBasePageData(pageEventsTitle, webEventsPath, r),
FsEventsSearchURL: webEventsFsSearchPath,
ProviderEventsSearchURL: webEventsProviderSearchPath,
+ LogEventsSearchURL: webEventsLogSearchPath,
}
renderAdminTemplate(w, templateEvents, data)
}
diff --git a/internal/metric/metric.go b/internal/metric/metric.go
index 534e9840..f1f2e9af 100644
--- a/internal/metric/metric.go
+++ b/internal/metric/metric.go
@@ -108,9 +108,9 @@ var (
Help: "The total number of login attempts",
})
- // totalNoAuthTryed is te metric that reports the total number of clients disconnected
+ // totalNoAuthTried is te metric that reports the total number of clients disconnected
// for inactivity before trying to login
- totalNoAuthTryed = promauto.NewCounter(prometheus.CounterOpts{
+ totalNoAuthTried = promauto.NewCounter(prometheus.CounterOpts{
Name: "sftpgo_no_auth_total",
Help: "The total number of clients disconnected for inactivity before trying to login",
})
@@ -984,10 +984,10 @@ func AddLoginResult(authMethod string, err error) {
}
}
-// AddNoAuthTryed increments the metric for clients disconnected
+// AddNoAuthTried increments the metric for clients disconnected
// for inactivity before trying to login
-func AddNoAuthTryed() {
- totalNoAuthTryed.Inc()
+func AddNoAuthTried() {
+ totalNoAuthTried.Inc()
}
// HTTPRequestServed increments the metrics for HTTP requests
diff --git a/internal/metric/metric_disabled.go b/internal/metric/metric_disabled.go
index 507897be..63369703 100644
--- a/internal/metric/metric_disabled.go
+++ b/internal/metric/metric_disabled.go
@@ -64,9 +64,9 @@ func AddLoginAttempt(_ string) {}
// AddLoginResult increments the metrics for login results
func AddLoginResult(_ string, _ error) {}
-// AddNoAuthTryed increments the metric for clients disconnected
+// AddNoAuthTried increments the metric for clients disconnected
// for inactivity before trying to login
-func AddNoAuthTryed() {}
+func AddNoAuthTried() {}
// HTTPRequestServed increments the metrics for HTTP requests
func HTTPRequestServed(_ int) {}
diff --git a/internal/plugin/notifier.go b/internal/plugin/notifier.go
index 9f0fb6c9..92722d63 100644
--- a/internal/plugin/notifier.go
+++ b/internal/plugin/notifier.go
@@ -33,6 +33,7 @@ type NotifierConfig struct {
FsEvents []string `json:"fs_events" mapstructure:"fs_events"`
ProviderEvents []string `json:"provider_events" mapstructure:"provider_events"`
ProviderObjects []string `json:"provider_objects" mapstructure:"provider_objects"`
+ LogEvents []int `json:"log_events" mapstructure:"log_events"`
RetryMaxTime int `json:"retry_max_time" mapstructure:"retry_max_time"`
RetryQueueMaxSize int `json:"retry_queue_max_size" mapstructure:"retry_queue_max_size"`
}
@@ -51,6 +52,7 @@ type eventsQueue struct {
sync.RWMutex
fsEvents []*notifier.FsEvent
providerEvents []*notifier.ProviderEvent
+ logEvents []*notifier.LogEvent
}
func (q *eventsQueue) addFsEvent(event *notifier.FsEvent) {
@@ -67,6 +69,13 @@ func (q *eventsQueue) addProviderEvent(event *notifier.ProviderEvent) {
q.providerEvents = append(q.providerEvents, event)
}
+func (q *eventsQueue) addLogEvent(event *notifier.LogEvent) {
+ q.Lock()
+ defer q.Unlock()
+
+ q.logEvents = append(q.logEvents, event)
+}
+
func (q *eventsQueue) popFsEvent() *notifier.FsEvent {
q.Lock()
defer q.Unlock()
@@ -97,11 +106,26 @@ func (q *eventsQueue) popProviderEvent() *notifier.ProviderEvent {
return ev
}
+func (q *eventsQueue) popLogEvent() *notifier.LogEvent {
+ q.Lock()
+ defer q.Unlock()
+
+ if len(q.logEvents) == 0 {
+ return nil
+ }
+ truncLen := len(q.logEvents) - 1
+ ev := q.logEvents[truncLen]
+ q.logEvents[truncLen] = nil
+ q.logEvents = q.logEvents[:truncLen]
+
+ return ev
+}
+
func (q *eventsQueue) getSize() int {
q.RLock()
defer q.RUnlock()
- return len(q.providerEvents) + len(q.fsEvents)
+ return len(q.providerEvents) + len(q.fsEvents) + len(q.logEvents)
}
type notifierPlugin struct {
@@ -225,6 +249,19 @@ 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()
+
+ p.sendLogEvent(event)
+ }()
+}
+
func (p *notifierPlugin) sendFsEvent(event *notifier.FsEvent) {
if err := p.notifier.NotifyFsEvent(event); err != nil {
logger.Warn(logSender, "", "unable to send fs action notification to plugin %v: %v", p.config.Cmd, err)
@@ -243,6 +280,15 @@ func (p *notifierPlugin) sendProviderEvent(event *notifier.ProviderEvent) {
}
}
+func (p *notifierPlugin) sendLogEvent(event *notifier.LogEvent) {
+ if err := p.notifier.NotifyLogEvent(event); err != nil {
+ logger.Warn(logSender, "", "unable to send log event to plugin %v: %v", p.config.Cmd, err)
+ if p.canQueueEvent(event.Timestamp) {
+ p.queue.addLogEvent(event)
+ }
+ }
+}
+
func (p *notifierPlugin) sendQueuedEvents() {
queueSize := p.queue.getSize()
if queueSize == 0 {
@@ -264,5 +310,12 @@ func (p *notifierPlugin) sendQueuedEvents() {
}(providerEv)
providerEv = p.queue.popProviderEvent()
}
+ logEv := p.queue.popLogEvent()
+ for logEv != nil {
+ go func(ev *notifier.LogEvent) {
+ p.sendLogEvent(ev)
+ }(logEv)
+ logEv = p.queue.popLogEvent()
+ }
logger.Debug(logSender, "", "queued events sent for notifier %q, new events size: %v", p.config.Cmd, p.queue.getSize())
}
diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go
index 970062d3..de6f27c7 100644
--- a/internal/plugin/plugin.go
+++ b/internal/plugin/plugin.go
@@ -291,15 +291,38 @@ func (m *Manager) NotifyProviderEvent(event *notifier.ProviderEvent, object Rend
}
}
+// NotifyLogEvent sends the log event notifications using any defined notifier plugins
+func (m *Manager) NotifyLogEvent(event notifier.LogEventType, protocol, username, ip, role string, err error) {
+ if !m.hasNotifiers {
+ return
+ }
+ 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,
+ }
+
+ for _, n := range m.notifiers {
+ n.notifyLogEvent(e)
+ }
+}
+
// HasSearcher returns true if an event searcher plugin is defined
func (m *Manager) HasSearcher() bool {
return m.hasSearcher
}
// SearchFsEvents returns the filesystem events matching the specified filters
-func (m *Manager) SearchFsEvents(searchFilters *eventsearcher.FsEventSearch) ([]byte, []string, []string, error) {
+func (m *Manager) SearchFsEvents(searchFilters *eventsearcher.FsEventSearch) ([]byte, error) {
if !m.hasSearcher {
- return nil, nil, nil, ErrNoSearcher
+ return nil, ErrNoSearcher
}
m.searcherLock.RLock()
plugin := m.searcher
@@ -309,9 +332,9 @@ func (m *Manager) SearchFsEvents(searchFilters *eventsearcher.FsEventSearch) ([]
}
// SearchProviderEvents returns the provider events matching the specified filters
-func (m *Manager) SearchProviderEvents(searchFilters *eventsearcher.ProviderEventSearch) ([]byte, []string, []string, error) {
+func (m *Manager) SearchProviderEvents(searchFilters *eventsearcher.ProviderEventSearch) ([]byte, error) {
if !m.hasSearcher {
- return nil, nil, nil, ErrNoSearcher
+ return nil, ErrNoSearcher
}
m.searcherLock.RLock()
plugin := m.searcher
@@ -320,6 +343,18 @@ func (m *Manager) SearchProviderEvents(searchFilters *eventsearcher.ProviderEven
return plugin.searchear.SearchProviderEvents(searchFilters)
}
+// SearchLogEvents returns the log events matching the specified filters
+func (m *Manager) SearchLogEvents(searchFilters *eventsearcher.LogEventSearch) ([]byte, error) {
+ if !m.hasSearcher {
+ return nil, ErrNoSearcher
+ }
+ m.searcherLock.RLock()
+ plugin := m.searcher
+ m.searcherLock.RUnlock()
+
+ return plugin.searchear.SearchLogEvents(searchFilters)
+}
+
// HasMetadater returns true if a metadata plugin is defined
func (m *Manager) HasMetadater() bool {
return m.hasMetadater
diff --git a/internal/sftpd/server.go b/internal/sftpd/server.go
index 72f357dd..eb6ae298 100644
--- a/internal/sftpd/server.go
+++ b/internal/sftpd/server.go
@@ -32,12 +32,14 @@ import (
"time"
"github.com/pkg/sftp"
+ "github.com/sftpgo/sdk/plugin/notifier"
"golang.org/x/crypto/ssh"
"github.com/drakkan/sftpgo/v2/internal/common"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
"github.com/drakkan/sftpgo/v2/internal/logger"
"github.com/drakkan/sftpgo/v2/internal/metric"
+ "github.com/drakkan/sftpgo/v2/internal/plugin"
"github.com/drakkan/sftpgo/v2/internal/util"
"github.com/drakkan/sftpgo/v2/internal/vfs"
)
@@ -762,19 +764,27 @@ func checkAuthError(ip string, err error) {
if errors.As(err, &sftpAuthErr) {
if sftpAuthErr.getLoginMethod() == dataprovider.SSHLoginMethodPublicKey {
event := common.HostEventLoginFailed
+ logEv := notifier.LogEventTypeLoginFailed
if errors.Is(err, util.ErrNotFound) {
event = common.HostEventUserNotFound
+ logEv = notifier.LogEventTypeLoginNoUser
}
common.AddDefenderEvent(ip, common.ProtocolSSH, event)
+ plugin.Handler.NotifyLogEvent(logEv, common.ProtocolSSH, "", ip, "", err)
return
}
}
}
} else {
- logger.ConnectionFailedLog("", ip, dataprovider.LoginMethodNoAuthTryed, common.ProtocolSSH, err.Error())
- metric.AddNoAuthTryed()
+ logger.ConnectionFailedLog("", ip, dataprovider.LoginMethodNoAuthTried, common.ProtocolSSH, err.Error())
+ metric.AddNoAuthTried()
common.AddDefenderEvent(ip, common.ProtocolSSH, common.HostEventNoLoginTried)
- dataprovider.ExecutePostLoginHook(&dataprovider.User{}, dataprovider.LoginMethodNoAuthTryed, ip, common.ProtocolSSH, err)
+ dataprovider.ExecutePostLoginHook(&dataprovider.User{}, dataprovider.LoginMethodNoAuthTried, ip, common.ProtocolSSH, err)
+ logEv := notifier.LogEventTypeNoLoginTried
+ if errors.Is(err, ssh.ErrNoCommonAlgo) {
+ logEv = notifier.LogEventTypeNotNegotiated
+ }
+ plugin.Handler.NotifyLogEvent(logEv, common.ProtocolSSH, "", ip, "", err)
}
}
@@ -1230,10 +1240,13 @@ func updateLoginMetrics(user *dataprovider.User, ip, method string, err error) {
// record failed login key auth only once for session if the
// authentication fails in checkAuthError
event := common.HostEventLoginFailed
+ logEv := notifier.LogEventTypeLoginFailed
if errors.Is(err, util.ErrNotFound) {
event = common.HostEventUserNotFound
+ logEv = notifier.LogEventTypeLoginNoUser
}
common.AddDefenderEvent(ip, common.ProtocolSSH, event)
+ plugin.Handler.NotifyLogEvent(logEv, common.ProtocolSSH, user.Username, ip, "", err)
}
}
metric.AddLoginResult(method, err)
diff --git a/internal/webdavd/file.go b/internal/webdavd/file.go
index a82a9c56..7f1cca9e 100644
--- a/internal/webdavd/file.go
+++ b/internal/webdavd/file.go
@@ -48,7 +48,7 @@ type webDavFile struct {
info os.FileInfo
startOffset int64
isFinished bool
- readTryed atomic.Bool
+ readTried atomic.Bool
}
func newWebDavFile(baseTransfer *common.BaseTransfer, pipeWriter *vfs.PipeWriter, pipeReader *pipeat.PipeReaderAt) *webDavFile {
@@ -70,7 +70,7 @@ func newWebDavFile(baseTransfer *common.BaseTransfer, pipeWriter *vfs.PipeWriter
startOffset: 0,
info: nil,
}
- f.readTryed.Store(false)
+ f.readTried.Store(false)
return f
}
@@ -177,7 +177,7 @@ func (f *webDavFile) checkFirstRead() error {
f.Connection.Log(logger.LevelDebug, "download for file %q denied by pre action: %v", f.GetVirtualPath(), err)
return f.Connection.GetPermissionDeniedError()
}
- f.readTryed.Store(true)
+ f.readTried.Store(true)
return nil
}
@@ -186,7 +186,7 @@ func (f *webDavFile) Read(p []byte) (n int, err error) {
if f.AbortTransfer.Load() {
return 0, errTransferAborted
}
- if !f.readTryed.Load() {
+ if !f.readTried.Load() {
if err := f.checkFirstRead(); err != nil {
return 0, err
}
@@ -417,7 +417,7 @@ func (f *webDavFile) setFinished() error {
func (f *webDavFile) isTransfer() bool {
if f.GetType() == common.TransferDownload {
- return f.readTryed.Load()
+ return f.readTried.Load()
}
return true
}
diff --git a/internal/webdavd/server.go b/internal/webdavd/server.go
index 43290d7b..ed424474 100644
--- a/internal/webdavd/server.go
+++ b/internal/webdavd/server.go
@@ -33,11 +33,13 @@ import (
"github.com/go-chi/chi/v5/middleware"
"github.com/rs/cors"
"github.com/rs/xid"
+ "github.com/sftpgo/sdk/plugin/notifier"
"github.com/drakkan/sftpgo/v2/internal/common"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
"github.com/drakkan/sftpgo/v2/internal/logger"
"github.com/drakkan/sftpgo/v2/internal/metric"
+ "github.com/drakkan/sftpgo/v2/internal/plugin"
"github.com/drakkan/sftpgo/v2/internal/util"
)
@@ -414,10 +416,13 @@ func updateLoginMetrics(user *dataprovider.User, ip, loginMethod string, err err
if err != nil && err != common.ErrInternalFailure && err != common.ErrNoCredentials {
logger.ConnectionFailedLog(user.Username, ip, loginMethod, common.ProtocolWebDAV, err.Error())
event := common.HostEventLoginFailed
+ logEv := notifier.LogEventTypeLoginFailed
if errors.Is(err, util.ErrNotFound) {
event = common.HostEventUserNotFound
+ logEv = notifier.LogEventTypeLoginNoUser
}
common.AddDefenderEvent(ip, common.ProtocolWebDAV, event)
+ plugin.Handler.NotifyLogEvent(logEv, common.ProtocolWebDAV, user.Username, ip, "", err)
}
metric.AddLoginResult(loginMethod, err)
dataprovider.ExecutePostLoginHook(user, loginMethod, ip, common.ProtocolWebDAV, err)
diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml
index 1ebe8c49..70c5b6f5 100644
--- a/openapi/openapi.yaml
+++ b/openapi/openapi.yaml
@@ -2594,13 +2594,10 @@ paths:
explode: false
required: false
- in: query
- name: exclude_ids
+ name: from_id
schema:
- type: array
- items:
- type: string
- description: 'the event id must not be included among those specified. This is useful for cursor based pagination. Empty or missing means omit this filter. Values must be specified comma separated'
- explode: false
+ type: string
+ description: 'the event id to start from. This is useful for cursor based pagination. Empty or missing means omit this filter.'
required: false
- in: query
name: role
@@ -2728,13 +2725,10 @@ paths:
explode: false
required: false
- in: query
- name: exclude_ids
+ name: from_id
schema:
- type: array
- items:
- type: string
- description: 'the event id must not be included among those specified. This is useful for cursor based pagination. Empty or missing means omit this filter. Values must be specified comma separated'
- explode: false
+ type: string
+ description: 'the event id to start from. This is useful for cursor based pagination. Empty or missing means omit this filter.'
required: false
- in: query
name: role
@@ -2797,6 +2791,131 @@ paths:
$ref: '#/components/responses/InternalServerError'
default:
$ref: '#/components/responses/DefaultResponse'
+ /events/log:
+ get:
+ tags:
+ - events
+ summary: Get log events
+ description: 'Returns an array with one or more log events applying the specified filters. This API is only available if you configure an "eventsearcher" plugin'
+ operationId: get_log_events
+ parameters:
+ - in: query
+ name: start_timestamp
+ schema:
+ type: integer
+ format: int64
+ minimum: 0
+ default: 0
+ required: false
+ description: 'the event timestamp, unix timestamp in nanoseconds, must be greater than or equal to the specified one. 0 or missing means omit this filter'
+ - in: query
+ name: end_timestamp
+ schema:
+ type: integer
+ format: int64
+ minimum: 0
+ default: 0
+ required: false
+ description: 'the event timestamp, unix timestamp in nanoseconds, must be less than or equal to the specified one. 0 or missing means omit this filter'
+ - in: query
+ name: events
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/LogEventType'
+ description: 'the log events must be included among those specified. Empty or missing means omit this filter. Events must be specified comma separated'
+ explode: false
+ required: false
+ - in: query
+ name: username
+ schema:
+ type: string
+ description: 'the event username must be the same as the one specified. Empty or missing means omit this filter'
+ required: false
+ - in: query
+ name: ip
+ schema:
+ type: string
+ description: 'the event IP must be the same as the one specified. Empty or missing means omit this filter'
+ required: false
+ - in: query
+ name: protocols
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/EventProtocols'
+ description: 'the event protocol must be included among those specified. Empty or missing means omit this filter. Values must be specified comma separated'
+ explode: false
+ required: false
+ - in: query
+ name: instance_ids
+ schema:
+ type: array
+ items:
+ type: string
+ description: 'the event instance id must be included among those specified. Empty or missing means omit this filter. Values must be specified comma separated'
+ explode: false
+ required: false
+ - in: query
+ name: from_id
+ schema:
+ type: string
+ description: 'the event id to start from. This is useful for cursor based pagination. Empty or missing means omit this filter.'
+ required: false
+ - in: query
+ name: role
+ schema:
+ type: string
+ description: 'User role. Empty or missing means omit this filter. Ignored if the admin has a role'
+ required: false
+ - in: query
+ name: csv_export
+ schema:
+ type: boolean
+ default: false
+ required: false
+ description: 'If enabled, events are exported as a CSV file'
+ - in: query
+ name: limit
+ schema:
+ type: integer
+ minimum: 1
+ maximum: 1000
+ default: 100
+ required: false
+ description: 'The maximum number of items to return. Max value is 1000, default is 100'
+ - in: query
+ name: order
+ required: false
+ description: Ordering events by timestamp. Default DESC
+ schema:
+ type: string
+ enum:
+ - ASC
+ - DESC
+ example: DESC
+ responses:
+ '200':
+ description: successful operation
+ content:
+ application/json; charset=utf-8:
+ schema:
+ type: array
+ items:
+ $ref: '#/components/schemas/LogEvent'
+ text/csv:
+ schema:
+ type: string
+ '400':
+ $ref: '#/components/responses/BadRequest'
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ $ref: '#/components/responses/Forbidden'
+ '500':
+ $ref: '#/components/responses/InternalServerError'
+ default:
+ $ref: '#/components/responses/DefaultResponse'
/apikeys:
get:
security:
@@ -5143,6 +5262,19 @@ components:
- roles
- ip_lists
- configs
+ LogEventType:
+ type: integer
+ enum:
+ - 1
+ - 2
+ - 3
+ - 4
+ description: >
+ Event status:
+ * `1` - Login failed
+ * `2` - Login failed non-existent user
+ * `3` - No login tried
+ * `4` - Algorithm negotiation failed
FsEventStatus:
type: integer
enum:
@@ -6787,6 +6919,8 @@ components:
type: string
open_flags:
type: string
+ role:
+ type: string
instance_id:
type: string
ProviderEvent:
@@ -6812,6 +6946,31 @@ components:
type: string
format: byte
description: 'base64 of the JSON serialized object with sensitive fields removed'
+ role:
+ type: string
+ instance_id:
+ type: string
+ LogEvent:
+ type: object
+ properties:
+ id:
+ type: string
+ timestamp:
+ type: integer
+ format: int64
+ description: 'unix timestamp in nanoseconds'
+ event:
+ $ref: '#/components/schemas/LogEventType'
+ protocol:
+ $ref: '#/components/schemas/EventProtocols'
+ username:
+ type: string
+ ip:
+ type: string
+ message:
+ type: string
+ role:
+ type: string
instance_id:
type: string
KeyValue:
diff --git a/templates/webadmin/events.html b/templates/webadmin/events.html
index dd0fc1b8..edcaf139 100644
--- a/templates/webadmin/events.html
+++ b/templates/webadmin/events.html
@@ -44,6 +44,7 @@ along with this program. If not, see
| ID | +Time | +Action | +User | +Proto | +IP | +Message | +
|---|