add support for log events

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2023-05-12 18:34:59 +02:00
parent 43d011f125
commit 4eded56d5f
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
34 changed files with 856 additions and 139 deletions

View file

@ -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.

View file

@ -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.

View file

@ -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

View file

@ -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)

28
go.mod
View file

@ -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
)

55
go.sum
View file

@ -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=

View file

@ -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())

View file

@ -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)
}
}
}

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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"

View file

@ -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)

View file

@ -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 ""
}
}

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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)
}

View file

@ -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

View file

@ -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) {}

View file

@ -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())
}

View file

@ -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 := &notifier.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

View file

@ -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)

View file

@ -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
}

View file

@ -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)

View file

@ -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:

View file

@ -44,6 +44,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<select class="form-control selectpicker" id="idEventType" name="events_type" onchange="onEventChanged(this.value)">
<option value="1" selected>Fs events</option>
<option value="2">Provider events</option>
<option value="3">Other events</option>
</select>
</div>
<div class="form-group col-md-3">
@ -59,7 +60,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</div>
<div class="form-row">
<div class="form-group col-md-4">
<select class="form-control selectpicker fs-events" id="idProtocols" name="protocols" title="Protocols" multiple>
<select class="form-control selectpicker fs-events" id="idStatuses" name="statuses" title="Statuses" multiple>
<option value="1">OK</option>
<option value="2">KO</option>
<option value="3">Quota exceeded</option>
</select>
</div>
<div class="form-group col-md-4">
<select class="form-control selectpicker fs-events log-events" id="idProtocols" name="protocols" title="Protocols" multiple>
<option value="SFTP">SFTP</option>
<option value="SCP">SCP</option>
<option value="SSH">SSH</option>
@ -72,13 +80,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<option value="EventAction">EventAction</option>
</select>
</div>
<div class="form-group col-md-4">
<select class="form-control selectpicker fs-events" id="idStatuses" name="statuses" title="Statuses" multiple>
<option value="1">OK</option>
<option value="2">KO</option>
<option value="3">Quota exceeded</option>
</select>
</div>
<div class="form-group col-md-4">
<div class="input-group">
<input type="text" id="dateTimeRange" class="form-control bg-light border-0" aria-describedby="search-button">
@ -129,6 +130,22 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
</table>
</div>
<div class="table-responsive log-events">
<table class="table table-hover nowrap" id="dataTableLog" width="100%" cellspacing="0">
<thead>
<tr>
<th>ID</th>
<th>Time</th>
<th>Action</th>
<th>User</th>
<th>Proto</th>
<th>IP</th>
<th>Message</th>
</tr>
</thead>
</table>
</div>
<div id="paginationContainer" class="m-4 d-none">
<nav aria-label="Pagination">
<ul class="pagination justify-content-end">
@ -160,6 +177,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
let dateFn = $.fn.dataTable.render.datetime();
let isFsDataTableInitialized = false;
let isProviderDataTableInitialized = false;
let isLogDataTableInitialized = false;
const pageSize = 20;
const paginationData = new Map();
@ -278,12 +296,18 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
return;
}
table = $('#dataTableFs').DataTable();
} else {
} else if (eventType == 2) {
if (!isProviderDataTableInitialized){
initProviderDatatable();
return;
}
table = $('#dataTableProvider').DataTable();
} else {
if (!isLogDataTableInitialized){
initLogDatatable();
return;
}
table = $('#dataTableLog').DataTable();
}
table.clear().draw();
table.ajax.url(getSearchURL(false)).load();
@ -313,15 +337,28 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
if (statuses.length > 0){
url+="&statuses="+encodeURIComponent(String(statuses));
}
} else {
} else if (eventType == 2) {
url = "{{.ProviderEventsSearchURL}}?omit_object_data=true&limit="+limit;
} else {
url = "{{.LogEventsSearchURL}}?limit="+limit;
let protocols = [];
$('#idProtocols').find('option:selected').each(function(){
protocols.push($(this).val());
});
if (protocols.length > 0){
url+="&protocols="+encodeURIComponent(String(protocols));
}
}
let actions = [];
$('#idActions').find('option:selected').each(function(){
actions.push($(this).val());
});
if (actions.length > 0){
url+="&actions="+encodeURIComponent(String(actions));
if (eventType == 3){
url+="&events="+encodeURIComponent(String(actions));
} else {
url+="&actions="+encodeURIComponent(String(actions));
}
}
let username = $('#idUsername').val();
if (username){
@ -332,26 +369,26 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
url+="&ip="+encodeURIComponent(ip);
}
let drp = $('#dateTimeRange').data('daterangepicker');
let excludeIds = [];
let fromID = "";
let start_ts;
if (!csvExport && paginationData.get("prevClicked") && paginationData.has("lastId") && paginationData.has("lastTs")){
order = "ASC";
start_ts = paginationData.get("lastTs");
excludeIds.push(paginationData.get("lastId"));
fromID = paginationData.get("lastId");
} else {
start_ts = drp.startDate.valueOf()*1000000;
}
let end_ts;
if (!csvExport && paginationData.get("nextClicked") && paginationData.has("firstId") && paginationData.has("firstTs")){
end_ts = paginationData.get("firstTs");
excludeIds.push(paginationData.get("firstId"));
fromID = paginationData.get("firstId");
} else {
end_ts = drp.endDate.valueOf()*1000000;
}
url+="&start_timestamp="+encodeURIComponent(start_ts);
url+="&end_timestamp="+encodeURIComponent(end_ts);
if (excludeIds.length > 0){
url+="&exclude_ids="+encodeURIComponent(String(excludeIds));
if (fromID){
url+="&from_id="+encodeURIComponent(fromID);
}
url+="&order="+order;
if (csvExport){
@ -360,6 +397,120 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
return url;
}
function initLogDatatable(){
$('#errorMsg').hide();
let tableLog = $('#dataTableLog').DataTable({
"ajax": {
"url": getSearchURL(false),
"dataSrc": handleResponseData,
"error": function ($xhr, textStatus, errorThrown) {
$(".dataTables_processing").hide();
let txt = "Failed to get log events";
if ($xhr) {
let json = $xhr.responseJSON;
if (json) {
if (json.message){
txt += ": " + json.message;
} else {
txt += ": " + json.error;
}
}
}
$('#errorTxt').text(txt);
$('#errorMsg').show();
}
},
"deferRender": true,
"processing": true,
"columns": [
{ "data": "id" },
{
"data": "timestamp",
"render": function (data, type, row) {
if (type === 'display') {
return dateFn(data/1000000,type);
}
return data;
}
},
{
"data": "event",
"render": function (data, type, row) {
if (type === 'display') {
switch (data){
case 1:
return "Login failed";
case 2:
return "Login with non-existent user";
case 3:
return "No login tried";
case 4:
return "Algorithm negotiation failed";
}
}
return data;
}
},
{
"data": "username",
"defaultContent": "",
"render": function (data, type, row) {
if (type === 'display') {
if (!data){
return "";
}
return escapeHTML(data);
}
return data;
}
},
{
"data": "protocol",
"defaultContent": ""
},
{
"data": "ip",
"defaultContent": ""
},
{
"data": "message",
"defaultContent": "",
"render": function (data, type, row) {
if (type === 'display') {
if (!data){
return "";
}
return '<span style="white-space:normal">' + escapeHTML(data) + "</span>"
}
return data;
}
}
],
"buttons": [],
"lengthChange": false,
"columnDefs": [
{
"targets": [0],
"visible": false,
"searchable": false
},
],
"responsive": true,
"searching": false,
"paging": false,
"info": false,
"ordering": false,
"language": {
"loadingRecords": "",
"emptyTable": "No logs found"
}
});
new $.fn.dataTable.FixedHeader(tableLog);
isLogDataTableInitialized = true;
}
function initProviderDatatable(){
$('#errorMsg').hide();
let tableProvider = $('#dataTableProvider').DataTable({
@ -597,10 +748,30 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
$('#idUsername').val("");
$('#idIp').val("");
$('.provider-events').hide();
$('.log-events').hide();
$('.fs-events').show();
onSearchClicked();
}
function selectLogEvents(){
let idActions = $('#idActions');
idActions.selectpicker('deselectAll');
idActions.find('option').remove();
idActions.find('li').remove();
idActions.append($('<option>').val('1').text('Login failed'));
idActions.append($('<option>').val('2').text('Login with non-existent user'));
idActions.append($('<option>').val('3').text('No login tried'));
idActions.append($('<option>').val('4').text('Algorithm negotiation failed'));
idActions.selectpicker('refresh');
$('#idUsername').val("");
$('#idIp').val("");
$('.provider-events').hide();
$('.fs-events').hide();
$('.log-events').show();
onSearchClicked();
}
function selectProviderEvents(){
let idActions = $('#idActions');
idActions.selectpicker('deselectAll');
@ -614,6 +785,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
$('#idUsername').val("");
$('#idIp').val("");
$('.fs-events').hide();
$('.log-events').hide();
$('.provider-events').show();
onSearchClicked();
}
@ -626,6 +798,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
case '2':
selectProviderEvents();
break;
case '3':
selectLogEvents();
break;
default:
console.log(`unsupported event type: ${val}`);
}

View file

@ -4,7 +4,7 @@ go 1.20
require (
github.com/hashicorp/go-plugin v1.4.10-0.20230403150917-e889c1ba1044
github.com/sftpgo/sdk v0.1.3
github.com/sftpgo/sdk v0.1.4-0.20230512160325-38e59551f700
)
require (
@ -16,10 +16,10 @@ require (
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/oklog/run v1.1.0 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
google.golang.org/grpc v1.54.0 // indirect
google.golang.org/grpc v1.55.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
)

View file

@ -31,13 +31,13 @@ github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
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/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -45,15 +45,15 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag=
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=

View file

@ -49,11 +49,23 @@ type providerEvent struct {
InstanceID string `json:"instance_id,omitempty"`
}
type logEvent struct {
ID string `json:"id" gorm:"primaryKey"`
Timestamp int64 `json:"timestamp"`
Event int `json:"event"`
Protocol string `json:"protocol,omitempty"`
Username string `json:"username,omitempty"`
IP string `json:"ip,omitempty"`
Message string `json:"message,omitempty"`
Role string `json:"role,omitempty"`
InstanceID string `json:"instance_id,omitempty"`
}
type Searcher struct{}
func (s *Searcher) SearchFsEvents(filters *eventsearcher.FsEventSearch) ([]byte, []string, []string, error) {
func (s *Searcher) SearchFsEvents(filters *eventsearcher.FsEventSearch) ([]byte, error) {
if filters.StartTimestamp < 0 {
return nil, nil, nil, errNotSupported
return nil, errNotSupported
}
results := []fsEvent{
@ -84,15 +96,15 @@ func (s *Searcher) SearchFsEvents(filters *eventsearcher.FsEventSearch) ([]byte,
data, err := json.Marshal(results)
if err != nil {
return nil, nil, nil, err
return nil, err
}
return data, nil, nil, nil
return data, nil
}
func (s *Searcher) SearchProviderEvents(filters *eventsearcher.ProviderEventSearch) ([]byte, []string, []string, error) {
func (s *Searcher) SearchProviderEvents(filters *eventsearcher.ProviderEventSearch) ([]byte, error) {
if filters.StartTimestamp < 0 {
return nil, nil, nil, errNotSupported
return nil, errNotSupported
}
var objectData []byte
@ -117,10 +129,36 @@ func (s *Searcher) SearchProviderEvents(filters *eventsearcher.ProviderEventSear
data, err := json.Marshal(results)
if err != nil {
return nil, nil, nil, err
return nil, err
}
return data, nil, nil, nil
return data, nil
}
func (s *Searcher) SearchLogEvents(filters *eventsearcher.LogEventSearch) ([]byte, error) {
if filters.StartTimestamp < 0 {
return nil, errNotSupported
}
results := []logEvent{
{
ID: "1",
Timestamp: 100,
Event: 1,
Protocol: "SSH",
IP: "127.0.1.1",
Message: "Invalid credentials",
Role: "role3",
InstanceID: "instance2",
},
}
data, err := json.Marshal(results)
if err != nil {
return nil, err
}
return data, nil
}
func main() {

View file

@ -16,10 +16,10 @@ require (
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/oklog/run v1.1.0 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
google.golang.org/grpc v1.54.0 // indirect
google.golang.org/grpc v1.55.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
)

View file

@ -36,8 +36,8 @@ github.com/sftpgo/sdk v0.1.3/go.mod h1:gDxDaU3rhp9Y92ddsE7SbQ8jdBNNWK1DKlp5eHXrs
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
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/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -45,15 +45,15 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag=
google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=