Quellcode durchsuchen

WebAdmin: allow to search and export event logs

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Nicola Murino vor 2 Jahren
Ursprung
Commit
75d911f29e

+ 15 - 15
go.mod

@@ -24,17 +24,17 @@ require (
 	github.com/fclairamb/ftpserverlib v0.20.1-0.20221012093027-95be4ae0c9a6
 	github.com/fclairamb/go-log v0.4.1
 	github.com/go-acme/lego/v4 v4.9.1
-	github.com/go-chi/chi/v5 v5.0.8-0.20221018120124-e5529d9db4d3
+	github.com/go-chi/chi/v5 v5.0.8
 	github.com/go-chi/jwtauth/v5 v5.1.0
 	github.com/go-chi/render v1.0.2
 	github.com/go-sql-driver/mysql v1.7.0
 	github.com/golang/mock v1.6.0
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
 	github.com/google/uuid v1.3.0
-	github.com/hashicorp/go-hclog v1.3.1
+	github.com/hashicorp/go-hclog v1.4.0
 	github.com/hashicorp/go-plugin v1.4.6
 	github.com/hashicorp/go-retryablehttp v0.7.1
-	github.com/jackc/pgx/v5 v5.1.1
+	github.com/jackc/pgx/v5 v5.2.0
 	github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
 	github.com/klauspost/compress v1.15.12
 	github.com/lestrrat-go/jwx/v2 v2.0.8
@@ -51,7 +51,7 @@ require (
 	github.com/rs/cors v1.8.3-0.20220619195839-da52b0701de5
 	github.com/rs/xid v1.4.0
 	github.com/rs/zerolog v1.28.0
-	github.com/sftpgo/sdk v0.1.3-0.20221203095324-2feef3600930
+	github.com/sftpgo/sdk v0.1.3-0.20221205110949-c15308d4236e
 	github.com/shirou/gopsutil/v3 v3.22.11
 	github.com/spf13/afero v1.9.3
 	github.com/spf13/cobra v1.6.1
@@ -67,19 +67,19 @@ require (
 	go.uber.org/automaxprocs v1.5.1
 	gocloud.dev v0.27.0
 	golang.org/x/crypto v0.3.0
-	golang.org/x/net v0.2.0
-	golang.org/x/oauth2 v0.2.0
-	golang.org/x/sys v0.2.0
-	golang.org/x/time v0.2.0
-	google.golang.org/api v0.103.0
+	golang.org/x/net v0.4.0
+	golang.org/x/oauth2 v0.3.0
+	golang.org/x/sys v0.3.0
+	golang.org/x/time v0.3.0
+	google.golang.org/api v0.104.0
 	gopkg.in/natefinch/lumberjack.v2 v2.0.0
 )
 
 require (
 	cloud.google.com/go v0.107.0 // indirect
-	cloud.google.com/go/compute v1.13.0 // indirect
+	cloud.google.com/go/compute v1.14.0 // indirect
 	cloud.google.com/go/compute/metadata v0.2.2 // indirect
-	cloud.google.com/go/iam v0.7.0 // indirect
+	cloud.google.com/go/iam v0.8.0 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.1 // indirect
 	github.com/ajg/form v1.5.1 // indirect
 	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
@@ -119,7 +119,7 @@ require (
 	github.com/jackc/pgpassfile v1.0.0 // indirect
 	github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
-	github.com/klauspost/cpuid/v2 v2.2.1 // indirect
+	github.com/klauspost/cpuid/v2 v2.2.2 // indirect
 	github.com/kr/fs v0.1.0 // indirect
 	github.com/lestrrat-go/blackmagic v1.0.1 // indirect
 	github.com/lestrrat-go/httpcc v1.0.1 // indirect
@@ -154,11 +154,11 @@ require (
 	github.com/yusufpapurcu/wmi v1.2.2 // indirect
 	go.opencensus.io v0.24.0 // indirect
 	golang.org/x/mod v0.7.0 // indirect
-	golang.org/x/text v0.4.0 // indirect
-	golang.org/x/tools v0.3.0 // indirect
+	golang.org/x/text v0.5.0 // indirect
+	golang.org/x/tools v0.4.0 // 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-20221202195650-67e5cbc046fd // indirect
+	google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37 // indirect
 	google.golang.org/grpc v1.51.0 // indirect
 	google.golang.org/protobuf v1.28.1 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect

+ 31 - 27
go.sum

@@ -50,8 +50,8 @@ cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6m
 cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s=
 cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=
 cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U=
-cloud.google.com/go/compute v1.13.0 h1:AYrLkB8NPdDRslNp4Jxmzrhdr03fUAIDbiGFjLWowoU=
-cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE=
+cloud.google.com/go/compute v1.14.0 h1:hfm2+FfxVmnRlh6LpB7cg1ZNU+5edAHmW679JePztk0=
+cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo=
 cloud.google.com/go/compute/metadata v0.2.2 h1:aWKAjYaBaOSrpKl57+jnS/3fJRQnxL7TvR/u1VVbt6k=
 cloud.google.com/go/compute/metadata v0.2.2/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
 cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
@@ -60,8 +60,8 @@ cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqCl
 cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
 cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c=
 cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=
-cloud.google.com/go/iam v0.7.0 h1:k4MuwOsS7zGJJ+QfZ5vBK8SgHBAvYN/23BWsiihJ1vs=
-cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg=
+cloud.google.com/go/iam v0.8.0 h1:E2osAkZzxI/+8pZcxVLcDtAQx/u+hZXVryUaYQ5O0Kk=
+cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE=
 cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA=
 cloud.google.com/go/kms v1.6.0 h1:OWRZzrPmOZUzurjI2FBGtgY2mB1WaJkqhw6oIwSj0Yg=
 cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs=
@@ -609,8 +609,8 @@ github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwv
 github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
 github.com/go-acme/lego/v4 v4.9.1 h1:n9Z5MQwANeGSQKlVE3bEh9SDvAySK9oVYOKCGCESqQE=
 github.com/go-acme/lego/v4 v4.9.1/go.mod h1:g3JRUyWS3L/VObpp4bCxzJftKyf/Wba8QrSSnoiqjg4=
-github.com/go-chi/chi/v5 v5.0.8-0.20221018120124-e5529d9db4d3 h1:qzwVVqrbdP93ZaSHy0yWQRYnig+t+j1OxnVtEs8SFuQ=
-github.com/go-chi/chi/v5 v5.0.8-0.20221018120124-e5529d9db4d3/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
+github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
+github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
 github.com/go-chi/jwtauth/v5 v5.1.0 h1:wJyf2YZ/ohPvNJBwPOzZaQbyzwgMZZceE1m8FOzXLeA=
 github.com/go-chi/jwtauth/v5 v5.1.0/go.mod h1:MA93hc1au3tAQwCKry+fI4LqJ5MIVN4XSsglOo+lSc8=
 github.com/go-chi/render v1.0.2 h1:4ER/udB0+fMWB2Jlf15RV3F4A2FDuYi/9f+lFttR/Lg=
@@ -914,8 +914,8 @@ github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/S
 github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
 github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
 github.com/hashicorp/go-hclog v0.12.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
-github.com/hashicorp/go-hclog v1.3.1 h1:vDwF1DFNZhntP4DAjuTpOw3uEgMUpXh1pB5fW9DqHpo=
-github.com/hashicorp/go-hclog v1.3.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
+github.com/hashicorp/go-hclog v1.4.0 h1:ctuWFGrhFha8BnnzxqeRGidlEcQkDyL5u8J8t5eA11I=
+github.com/hashicorp/go-hclog v1.4.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
 github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
 github.com/hashicorp/go-immutable-radix v1.2.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
 github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
@@ -1014,8 +1014,8 @@ github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQ
 github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
 github.com/jackc/pgx/v4 v4.16.0/go.mod h1:N0A9sFdWzkw/Jy1lwoiB64F2+ugFZi987zRxcPez/wI=
 github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ=
-github.com/jackc/pgx/v5 v5.1.1 h1:pZD79K1SYv8wc2HmCQA6VdmRQi7/OtCfv9bM3WAXUYA=
-github.com/jackc/pgx/v5 v5.1.1/go.mod h1:Ptn7zmohNsWEsdxRawMzk3gaKma2obW+NWTnKa0S4nk=
+github.com/jackc/pgx/v5 v5.2.0 h1:NdPpngX0Y6z6XDFKqmFQaE+bCtkqzvQIOt1wvBlAqs8=
+github.com/jackc/pgx/v5 v5.2.0/go.mod h1:Ptn7zmohNsWEsdxRawMzk3gaKma2obW+NWTnKa0S4nk=
 github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
@@ -1064,8 +1064,9 @@ github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47e
 github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM=
 github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
 github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
-github.com/klauspost/cpuid/v2 v2.2.1 h1:U33DW0aiEj633gHYw3LoDNfkDiYnE5Q8M/TKJn2f2jI=
 github.com/klauspost/cpuid/v2 v2.2.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
+github.com/klauspost/cpuid/v2 v2.2.2 h1:xPMwiykqNK9VK0NYC3+jTMYv9I6Vl3YdjZgPZKG3zO0=
+github.com/klauspost/cpuid/v2 v2.2.2/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
 github.com/kolo/xmlrpc v0.0.0-20201022064351-38db28db192b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
 github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -1449,8 +1450,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-0.20221203095324-2feef3600930 h1:znJ52fQBSAQhacaQvZAfkpTioqpcutDREM/H8NttKzU=
-github.com/sftpgo/sdk v0.1.3-0.20221203095324-2feef3600930/go.mod h1:S2S/Q9fgUpXmL11YoCCt0hyCkEwH1LzQM/6QVsbUCFg=
+github.com/sftpgo/sdk v0.1.3-0.20221205110949-c15308d4236e h1:F3G/BReUSU8TX6Kmk0moQgQAk9Ouiv2I+pg//o1IR6U=
+github.com/sftpgo/sdk v0.1.3-0.20221205110949-c15308d4236e/go.mod h1:S2S/Q9fgUpXmL11YoCCt0hyCkEwH1LzQM/6QVsbUCFg=
 github.com/shirou/gopsutil/v3 v3.22.11 h1:kxsPKS+Eeo+VnEQ2XCaGJepeP6KY53QoRTETx3+1ndM=
 github.com/shirou/gopsutil/v3 v3.22.11/go.mod h1:xl0EeL4vXJ+hQMAGN8B9VFpxukEMA0XdevQOe5MZ1oY=
 github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
@@ -1814,8 +1815,9 @@ golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.0.0-20220802222814-0bcc04d9c69b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
-golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
 golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
+golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
+golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
 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=
@@ -1842,8 +1844,8 @@ golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7Lm
 golang.org/x/oauth2 v0.0.0-20220628200809-02e64fa58f26/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
 golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
 golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
-golang.org/x/oauth2 v0.2.0 h1:GtQkldQ9m7yvzCL1V+LrYow3Khe0eJH0w7RbX/VbaIU=
-golang.org/x/oauth2 v0.2.0/go.mod h1:Cwn6afJ8jrQwYMxQDTpISoXmXW9I6qF6vDeuuoX3Ibs=
+golang.org/x/oauth2 v0.3.0 h1:6l90koy8/LaBLmLu8jpHeHexzMwEita0zFfYlggy2F8=
+golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk=
 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=
@@ -2005,14 +2007,15 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
+golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
 golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
+golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -2022,8 +2025,9 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
+golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -2036,8 +2040,8 @@ golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxb
 golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.2.0 h1:52I/1L54xyEQAYdtcSuxtiT84KGYTBGXwayxmIpNJhE=
-golang.org/x/time v0.2.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
+golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -2122,8 +2126,8 @@ golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
 golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
 golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM=
-golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
+golang.org/x/tools v0.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4=
+golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
 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=
@@ -2183,8 +2187,8 @@ google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6F
 google.golang.org/api v0.86.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
 google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
 google.golang.org/api v0.91.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
-google.golang.org/api v0.103.0 h1:9yuVqlu2JCvcLg9p8S3fcFLZij8EPSyvODIY1rkMizQ=
-google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0=
+google.golang.org/api v0.104.0 h1:KBfmLRqdZEbwQleFlSLnzpQJwhjpmNOk4cKQIBDZ9mg=
+google.golang.org/api v0.104.0/go.mod h1:JCspTXJbBxa5ySXw4UgUqVer7DfVxbvc/CTUFqAED5U=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -2295,8 +2299,8 @@ google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljW
 google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
 google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
 google.golang.org/genproto v0.0.0-20220802133213-ce4fa296bf78/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc=
-google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd h1:OjndDrsik+Gt+e6fs45z9AxiewiKyLKYpA45W5Kpkks=
-google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE=
+google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37 h1:jmIfw8+gSvXcZSgaFAGyInDXeWzUhvYH57G/5GKMn70=
+google.golang.org/genproto v0.0.0-20221207170731-23e4bf6bdc37/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
 google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
 google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=

+ 172 - 3
internal/httpd/api_events.go

@@ -15,9 +15,13 @@
 package httpd
 
 import (
+	"encoding/csv"
+	"encoding/json"
 	"fmt"
 	"net/http"
 	"strconv"
+	"strings"
+	"time"
 
 	"github.com/sftpgo/sdk/plugin/eventsearcher"
 
@@ -88,7 +92,6 @@ func getFsSearchParamsFromRequest(r *http.Request) (eventsearcher.FsEventSearch,
 		}
 		s.FsProvider = val
 	}
-	s.IP = r.URL.Query().Get("ip")
 	s.SSHCmd = r.URL.Query().Get("ssh_cmd")
 	s.Bucket = r.URL.Query().Get("bucket")
 	s.Endpoint = r.URL.Query().Get("endpoint")
@@ -132,6 +135,14 @@ func searchFsEvents(w http.ResponseWriter, r *http.Request) {
 	}
 	filters.Role = getRoleFilterForEventSearch(r, claims.Role)
 
+	if getBoolQueryParam(r, "csv_export") {
+		filters.Limit = 100
+		if err := exportFsEvents(w, &filters); err != nil {
+			panic(http.ErrAbortHandler)
+		}
+		return
+	}
+
 	data, _, _, err := plugin.Handler.SearchFsEvents(&filters)
 	if err != nil {
 		sendAPIResponse(w, r, err, "", getRespStatus(err))
@@ -150,13 +161,21 @@ func searchProviderEvents(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	filters, err := getProviderSearchParamsFromRequest(r)
-	if err != nil {
+	var filters eventsearcher.ProviderEventSearch
+	if filters, err = getProviderSearchParamsFromRequest(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 := exportProviderEvents(w, &filters); err != nil {
+			panic(http.ErrAbortHandler)
+		}
+		return
+	}
+
 	data, _, _, err := plugin.Handler.SearchProviderEvents(&filters)
 	if err != nil {
 		sendAPIResponse(w, r, err, "", getRespStatus(err))
@@ -167,9 +186,159 @@ func searchProviderEvents(w http.ResponseWriter, r *http.Request) {
 	w.Write(data) //nolint:errcheck
 }
 
+func exportFsEvents(w http.ResponseWriter, filters *eventsearcher.FsEventSearch) error {
+	w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=fslogs-%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)
+
+	csvWriter := csv.NewWriter(w)
+	ev := fsEvent{}
+	err := csvWriter.Write(ev.getCSVHeader())
+	if err != nil {
+		return err
+	}
+	results := make([]fsEvent, 0, filters.Limit)
+	for {
+		data, _, _, err := plugin.Handler.SearchFsEvents(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) == 0 || len(results) < filters.Limit {
+			break
+		}
+		filters.StartTimestamp = results[len(results)-1].Timestamp
+		filters.ExcludeIDs = []string{results[len(results)-1].ID}
+		results = nil
+	}
+	csvWriter.Flush()
+	return csvWriter.Error()
+}
+
+func exportProviderEvents(w http.ResponseWriter, filters *eventsearcher.ProviderEventSearch) error {
+	w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=providerlogs-%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 := providerEvent{}
+	csvWriter := csv.NewWriter(w)
+	err := csvWriter.Write(ev.getCSVHeader())
+	if err != nil {
+		return err
+	}
+	results := make([]providerEvent, 0, filters.Limit)
+	for {
+		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) == 0 || len(results) < filters.Limit {
+			break
+		}
+		filters.StartTimestamp = results[len(results)-1].Timestamp
+		filters.ExcludeIDs = []string{results[len(results)-1].ID}
+		results = nil
+	}
+	csvWriter.Flush()
+	return csvWriter.Error()
+}
+
 func getRoleFilterForEventSearch(r *http.Request, defaultValue string) string {
 	if defaultValue != "" {
 		return defaultValue
 	}
 	return r.URL.Query().Get("role")
 }
+
+type fsEvent struct {
+	ID                string `json:"id"`
+	Timestamp         int64  `json:"timestamp"`
+	Action            string `json:"action"`
+	Username          string `json:"username"`
+	FsPath            string `json:"fs_path"`
+	FsTargetPath      string `json:"fs_target_path,omitempty"`
+	VirtualPath       string `json:"virtual_path"`
+	VirtualTargetPath string `json:"virtual_target_path,omitempty"`
+	SSHCmd            string `json:"ssh_cmd,omitempty"`
+	FileSize          int64  `json:"file_size,omitempty"`
+	Status            int    `json:"status"`
+	Protocol          string `json:"protocol"`
+	IP                string `json:"ip,omitempty"`
+	SessionID         string `json:"session_id"`
+	FsProvider        int    `json:"fs_provider"`
+	Bucket            string `json:"bucket,omitempty"`
+	Endpoint          string `json:"endpoint,omitempty"`
+	OpenFlags         int    `json:"open_flags,omitempty"`
+	Role              string `json:"role,omitempty"`
+	InstanceID        string `json:"instance_id,omitempty"`
+}
+
+func (e *fsEvent) getCSVHeader() []string {
+	return []string{"Time", "Action", "Path", "Size", "Status", "User", "Protocol",
+		"IP", "SSH command"}
+}
+
+func (e *fsEvent) getCSVData() []string {
+	timestamp := time.Unix(0, e.Timestamp).UTC()
+	var pathInfo strings.Builder
+	pathInfo.Write([]byte(e.VirtualPath))
+	if e.VirtualTargetPath != "" {
+		pathInfo.WriteString(" => ")
+		pathInfo.WriteString(e.VirtualTargetPath)
+	}
+	var status string
+	switch e.Status {
+	case 1:
+		status = "OK"
+	case 2:
+		status = "KO"
+	case 3:
+		status = "Quota exceeded"
+	}
+	var fileSize string
+	if e.FileSize > 0 {
+		fileSize = util.ByteCountIEC(e.FileSize)
+	}
+	return []string{timestamp.Format(time.RFC3339), e.Action, pathInfo.String(),
+		fileSize, status, e.Username, e.Protocol, e.IP, e.SSHCmd}
+}
+
+type providerEvent struct {
+	ID         string `json:"id"`
+	Timestamp  int64  `json:"timestamp"`
+	Action     string `json:"action"`
+	Username   string `json:"username"`
+	IP         string `json:"ip,omitempty"`
+	ObjectType string `json:"object_type"`
+	ObjectName string `json:"object_name"`
+	ObjectData []byte `json:"object_data"`
+	Role       string `json:"role,omitempty"`
+	InstanceID string `json:"instance_id,omitempty"`
+}
+
+func (e *providerEvent) getCSVHeader() []string {
+	return []string{"Time", "Action", "Object Type", "Object Name", "User", "IP"}
+}
+
+func (e *providerEvent) getCSVData() []string {
+	timestamp := time.Unix(0, e.Timestamp).UTC()
+	return []string{timestamp.Format(time.RFC3339), e.Action, e.ObjectType, e.ObjectName,
+		e.Username, e.IP}
+}

+ 9 - 0
internal/httpd/httpd.go

@@ -139,6 +139,9 @@ const (
 	webTemplateFolderDefault              = "/web/admin/template/folder"
 	webDefenderPathDefault                = "/web/admin/defender"
 	webDefenderHostsPathDefault           = "/web/admin/defender/hosts"
+	webEventsPathDefault                  = "/web/admin/events"
+	webEventsFsSearchPathDefault          = "/web/admin/events/fs"
+	webEventsProviderSearchPathDefault    = "/web/admin/events/provider"
 	webClientLoginPathDefault             = "/web/client/login"
 	webClientOIDCLoginPathDefault         = "/web/client/oidclogin"
 	webClientTwoFactorPathDefault         = "/web/client/twofactor"
@@ -226,6 +229,9 @@ var (
 	webTemplateUser                string
 	webTemplateFolder              string
 	webDefenderPath                string
+	webEventsPath                  string
+	webEventsFsSearchPath          string
+	webEventsProviderSearchPath    string
 	webDefenderHostsPath           string
 	webClientLoginPath             string
 	webClientOIDCLoginPath         string
@@ -1025,6 +1031,9 @@ func updateWebAdminURLs(baseURL string) {
 	webTemplateFolder = path.Join(baseURL, webTemplateFolderDefault)
 	webDefenderHostsPath = path.Join(baseURL, webDefenderHostsPathDefault)
 	webDefenderPath = path.Join(baseURL, webDefenderPathDefault)
+	webEventsPath = path.Join(baseURL, webEventsPathDefault)
+	webEventsFsSearchPath = path.Join(baseURL, webEventsFsSearchPathDefault)
+	webEventsProviderSearchPath = path.Join(baseURL, webEventsProviderSearchPathDefault)
 	webStaticFilesPath = path.Join(baseURL, webStaticFilesPathDefault)
 	webOpenAPIPath = path.Join(baseURL, webOpenAPIPathDefault)
 }

+ 49 - 1
internal/httpd/httpd_test.go

@@ -162,6 +162,7 @@ const (
 	webAdminEventActionPath        = "/web/admin/eventaction"
 	webAdminRolesPath              = "/web/admin/roles"
 	webAdminRolePath               = "/web/admin/role"
+	webEventsPath                  = "/web/admin/events"
 	webBasePathClient              = "/web/client"
 	webClientLoginPath             = "/web/client/login"
 	webClientFilesPath             = "/web/client/files"
@@ -8791,6 +8792,8 @@ func TestWebUserTwoFactorLogin(t *testing.T) {
 func TestSearchEvents(t *testing.T) {
 	token, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
 	assert.NoError(t, err)
+	webToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
+	assert.NoError(t, err)
 
 	req, err := http.NewRequest(http.MethodGet, fsEventsPath+"?limit=10&order=ASC&fs_provider=0", nil)
 	assert.NoError(t, err)
@@ -8817,13 +8820,32 @@ func TestSearchEvents(t *testing.T) {
 	err = json.Unmarshal(rr.Body.Bytes(), &events)
 	assert.NoError(t, err)
 	assert.Len(t, events, 1)
-
+	// CSV export
+	req, err = http.NewRequest(http.MethodGet, fsEventsPath+"?limit=10&order=ASC&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, fsEventsPath+"?start_timestamp=-1&end_timestamp=123456&statuses=1,2", 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, fsEventsPath+"?start_timestamp=-1&csv_export=true", nil)
+		assert.NoError(t, err)
+		setBearerForReq(req, token)
+		rr = executeRequest(req)
+	}
+	exportFunc()
 
 	req, err = http.NewRequest(http.MethodGet, fsEventsPath+"?limit=a", nil)
 	assert.NoError(t, err)
@@ -8847,6 +8869,13 @@ func TestSearchEvents(t *testing.T) {
 			assert.True(t, ok, field)
 		}
 	}
+	// CSV export
+	req, err = http.NewRequest(http.MethodGet, providerEventsPath+"?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, providerEventsPath+"?start_timestamp=-1", nil)
@@ -8854,6 +8883,19 @@ func TestSearchEvents(t *testing.T) {
 	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, providerEventsPath+"?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)
@@ -8890,6 +8932,12 @@ func TestSearchEvents(t *testing.T) {
 	setBearerForReq(req, token)
 	rr = executeRequest(req)
 	checkResponseCode(t, http.StatusBadRequest, rr)
+
+	req, err = http.NewRequest(http.MethodGet, webEventsPath, nil)
+	assert.NoError(t, err)
+	setJWTCookieForReq(req, webToken)
+	rr = executeRequest(req)
+	checkResponseCode(t, http.StatusOK, rr)
 }
 
 func TestMFAErrors(t *testing.T) {

+ 14 - 0
internal/httpd/internal_test.go

@@ -2911,6 +2911,20 @@ func TestEventRoleFilter(t *testing.T) {
 	assert.Equal(t, "role1", role)
 }
 
+func TestEventsCSV(t *testing.T) {
+	e := fsEvent{
+		Status: 1,
+	}
+	data := e.getCSVData()
+	assert.Equal(t, "OK", data[4])
+	e.Status = 2
+	data = e.getCSVData()
+	assert.Equal(t, "KO", data[4])
+	e.Status = 3
+	data = e.getCSVData()
+	assert.Equal(t, "Quota exceeded", data[4])
+}
+
 func isSharedProviderSupported() bool {
 	// SQLite shares the implementation with other SQL-based provider but it makes no sense
 	// to use it outside test cases

+ 6 - 0
internal/httpd/server.go

@@ -1662,6 +1662,12 @@ func (s *httpdServer) setupWebAdminRoutes() {
 				s.handleWebUpdateRolePost)
 			router.With(s.checkPerm(dataprovider.PermAdminManageRoles), verifyCSRFHeader).
 				Delete(webAdminRolePath+"/{name}", deleteRole)
+			router.With(s.checkPerm(dataprovider.PermAdminViewEvents), s.refreshCookie).Get(webEventsPath,
+				s.handleWebGetEvents)
+			router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler, s.refreshCookie).
+				Get(webEventsFsSearchPath, searchFsEvents)
+			router.With(s.checkPerm(dataprovider.PermAdminViewEvents), compressor.Handler, s.refreshCookie).
+				Get(webEventsProviderSearchPath, searchProviderEvents)
 		})
 	}
 }

+ 33 - 0
internal/httpd/webadmin.go

@@ -36,6 +36,7 @@ import (
 	"github.com/drakkan/sftpgo/v2/internal/dataprovider"
 	"github.com/drakkan/sftpgo/v2/internal/kms"
 	"github.com/drakkan/sftpgo/v2/internal/mfa"
+	"github.com/drakkan/sftpgo/v2/internal/plugin"
 	"github.com/drakkan/sftpgo/v2/internal/smtp"
 	"github.com/drakkan/sftpgo/v2/internal/util"
 	"github.com/drakkan/sftpgo/v2/internal/version"
@@ -86,6 +87,7 @@ const (
 	templateEventAction      = "eventaction.html"
 	templateRoles            = "roles.html"
 	templateRole             = "role.html"
+	templateEvents           = "events.html"
 	templateMessage          = "message.html"
 	templateStatus           = "status.html"
 	templateLogin            = "login.html"
@@ -108,6 +110,7 @@ const (
 	pageChangePwdTitle       = "Change password"
 	pageMaintenanceTitle     = "Maintenance"
 	pageDefenderTitle        = "Defender"
+	pageEventsTitle          = "Logs"
 	pageForgotPwdTitle       = "SFTPGo Admin - Forgot password"
 	pageResetPwdTitle        = "SFTPGo Admin - Reset password"
 	pageSetupTitle           = "Create first admin user"
@@ -135,6 +138,7 @@ type basePage struct {
 	FolderURL          string
 	FolderTemplateURL  string
 	DefenderURL        string
+	EventsURL          string
 	LogoutURL          string
 	ProfileURL         string
 	ChangePwdURL       string
@@ -160,10 +164,12 @@ type basePage struct {
 	StatusTitle        string
 	MaintenanceTitle   string
 	DefenderTitle      string
+	EventsTitle        string
 	Version            string
 	CSRFToken          string
 	IsEventManagerPage bool
 	HasDefender        bool
+	HasSearcher        bool
 	HasExternalLogin   bool
 	LoggedAdmin        *dataprovider.Admin
 	Branding           UIBranding
@@ -349,6 +355,12 @@ type eventRulePage struct {
 	IsShared        bool
 }
 
+type eventsPage struct {
+	basePage
+	FsEventsSearchURL       string
+	ProviderEventsSearchURL string
+}
+
 type messagePage struct {
 	basePage
 	Error   string
@@ -505,6 +517,11 @@ func loadAdminTemplates(templatesPath string) {
 		filepath.Join(templatesPath, templateAdminDir, templateBase),
 		filepath.Join(templatesPath, templateAdminDir, templateRole),
 	}
+	eventsPaths := []string{
+		filepath.Join(templatesPath, templateCommonDir, templateCommonCSS),
+		filepath.Join(templatesPath, templateAdminDir, templateBase),
+		filepath.Join(templatesPath, templateAdminDir, templateEvents),
+	}
 
 	fsBaseTpl := template.New("fsBaseTemplate").Funcs(template.FuncMap{
 		"ListFSProviders": func() []sdk.FilesystemProvider {
@@ -543,6 +560,7 @@ func loadAdminTemplates(templatesPath string) {
 	resetPwdTmpl := util.LoadTemplate(nil, resetPwdPaths...)
 	rolesTmpl := util.LoadTemplate(nil, rolesPaths...)
 	roleTmpl := util.LoadTemplate(nil, rolePaths...)
+	eventsTmpl := util.LoadTemplate(nil, eventsPaths...)
 
 	adminTemplates[templateUsers] = usersTmpl
 	adminTemplates[templateUser] = userTmpl
@@ -572,6 +590,7 @@ func loadAdminTemplates(templatesPath string) {
 	adminTemplates[templateResetPassword] = resetPwdTmpl
 	adminTemplates[templateRoles] = rolesTmpl
 	adminTemplates[templateRole] = roleTmpl
+	adminTemplates[templateEvents] = eventsTmpl
 }
 
 func isEventManagerResource(currentURL string) bool {
@@ -609,6 +628,7 @@ func (s *httpdServer) getBasePageData(title, currentURL string, r *http.Request)
 		FolderURL:          webFolderPath,
 		FolderTemplateURL:  webTemplateFolder,
 		DefenderURL:        webDefenderPath,
+		EventsURL:          webEventsPath,
 		LogoutURL:          webLogoutPath,
 		ProfileURL:         webAdminProfilePath,
 		ChangePwdURL:       webChangeAdminPwdPath,
@@ -636,10 +656,12 @@ func (s *httpdServer) getBasePageData(title, currentURL string, r *http.Request)
 		StatusTitle:        pageStatusTitle,
 		MaintenanceTitle:   pageMaintenanceTitle,
 		DefenderTitle:      pageDefenderTitle,
+		EventsTitle:        pageEventsTitle,
 		Version:            version.GetAsString(),
 		LoggedAdmin:        getAdminFromToken(r),
 		IsEventManagerPage: isEventManagerResource(currentURL),
 		HasDefender:        common.Config.DefenderConfig.Enabled,
+		HasSearcher:        plugin.Handler.HasSearcher(),
 		HasExternalLogin:   isLoggedInWithOIDC(r),
 		CSRFToken:          csrfToken,
 		Branding:           s.binding.Branding.WebAdmin,
@@ -3636,3 +3658,14 @@ func (s *httpdServer) handleWebUpdateRolePost(w http.ResponseWriter, r *http.Req
 	}
 	http.Redirect(w, r, webAdminRolesPath, http.StatusSeeOther)
 }
+
+func (s *httpdServer) handleWebGetEvents(w http.ResponseWriter, r *http.Request) {
+	r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
+
+	data := eventsPage{
+		basePage:                s.getBasePageData(pageEventsTitle, webEventsPath, r),
+		FsEventsSearchURL:       webEventsFsSearchPath,
+		ProviderEventsSearchURL: webEventsProviderSearchPath,
+	}
+	renderAdminTemplate(w, templateEvents, data)
+}

+ 5 - 0
internal/plugin/plugin.go

@@ -291,6 +291,11 @@ func (m *Manager) NotifyProviderEvent(event *notifier.ProviderEvent, object Rend
 	}
 }
 
+// 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) {
 	if !m.hasSearcher {

+ 25 - 2
openapi/openapi.yaml

@@ -2305,6 +2305,13 @@ paths:
             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:
@@ -2313,7 +2320,7 @@ paths:
             maximum: 1000
             default: 100
           required: false
-          description: 'The maximum number of items to return. Max value is 500, default is 100'
+          description: 'The maximum number of items to return. Max value is 1000, default is 100'
         - in: query
           name: order
           required: false
@@ -2333,6 +2340,9 @@ paths:
                 type: array
                 items:
                   $ref: '#/components/schemas/FsEvent'
+            text/csv:
+              schema:
+                type: string
         '400':
           $ref: '#/components/responses/BadRequest'
         '401':
@@ -2429,6 +2439,13 @@ paths:
             type: string
           description: 'Admin 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:
@@ -2437,7 +2454,7 @@ paths:
             maximum: 1000
             default: 100
           required: false
-          description: 'The maximum number of items to return. Max value is 500, default is 100'
+          description: 'The maximum number of items to return. Max value is 1000, default is 100'
         - in: query
           name: order
           required: false
@@ -2457,6 +2474,9 @@ paths:
                 type: array
                 items:
                   $ref: '#/components/schemas/ProviderEvent'
+            text/csv:
+              schema:
+                type: string
         '400':
           $ref: '#/components/responses/BadRequest'
         '401':
@@ -4616,16 +4636,19 @@ components:
         - HTTP
         - HTTPShare
         - DataRetention
+        - EventAction
         - OIDC
       description: |
         Protocols:
           * `SSH` - SSH commands
           * `SFTP` - SFTP protocol
+          * `SCP` - SCP protocol
           * `FTP` - plain FTP and FTPES/FTPS
           * `DAV` - WebDAV
           * `HTTP` - WebClient/REST API
           * `HTTPShare` - the event is generated in a public share
           * `DataRetention` - the event is generated by a data retention check
+          * `EventAction` - the event is generated by an EventManager action
           * `OIDC` - OpenID Connect
     WebClientOptions:
       type: string

+ 410 - 0
static/vendor/daterangepicker/daterangepicker.css

@@ -0,0 +1,410 @@
+.daterangepicker {
+  position: absolute;
+  color: inherit;
+  background-color: #fff;
+  border-radius: 4px;
+  border: 1px solid #ddd;
+  width: 278px;
+  max-width: none;
+  padding: 0;
+  margin-top: 7px;
+  top: 100px;
+  left: 20px;
+  z-index: 3001;
+  display: none;
+  font-family: arial;
+  font-size: 15px;
+  line-height: 1em;
+}
+
+.daterangepicker:before, .daterangepicker:after {
+  position: absolute;
+  display: inline-block;
+  border-bottom-color: rgba(0, 0, 0, 0.2);
+  content: '';
+}
+
+.daterangepicker:before {
+  top: -7px;
+  border-right: 7px solid transparent;
+  border-left: 7px solid transparent;
+  border-bottom: 7px solid #ccc;
+}
+
+.daterangepicker:after {
+  top: -6px;
+  border-right: 6px solid transparent;
+  border-bottom: 6px solid #fff;
+  border-left: 6px solid transparent;
+}
+
+.daterangepicker.opensleft:before {
+  right: 9px;
+}
+
+.daterangepicker.opensleft:after {
+  right: 10px;
+}
+
+.daterangepicker.openscenter:before {
+  left: 0;
+  right: 0;
+  width: 0;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.daterangepicker.openscenter:after {
+  left: 0;
+  right: 0;
+  width: 0;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.daterangepicker.opensright:before {
+  left: 9px;
+}
+
+.daterangepicker.opensright:after {
+  left: 10px;
+}
+
+.daterangepicker.drop-up {
+  margin-top: -7px;
+}
+
+.daterangepicker.drop-up:before {
+  top: initial;
+  bottom: -7px;
+  border-bottom: initial;
+  border-top: 7px solid #ccc;
+}
+
+.daterangepicker.drop-up:after {
+  top: initial;
+  bottom: -6px;
+  border-bottom: initial;
+  border-top: 6px solid #fff;
+}
+
+.daterangepicker.single .daterangepicker .ranges, .daterangepicker.single .drp-calendar {
+  float: none;
+}
+
+.daterangepicker.single .drp-selected {
+  display: none;
+}
+
+.daterangepicker.show-calendar .drp-calendar {
+  display: block;
+}
+
+.daterangepicker.show-calendar .drp-buttons {
+  display: block;
+}
+
+.daterangepicker.auto-apply .drp-buttons {
+  display: none;
+}
+
+.daterangepicker .drp-calendar {
+  display: none;
+  max-width: 270px;
+}
+
+.daterangepicker .drp-calendar.left {
+  padding: 8px 0 8px 8px;
+}
+
+.daterangepicker .drp-calendar.right {
+  padding: 8px;
+}
+
+.daterangepicker .drp-calendar.single .calendar-table {
+  border: none;
+}
+
+.daterangepicker .calendar-table .next span, .daterangepicker .calendar-table .prev span {
+  color: #fff;
+  border: solid black;
+  border-width: 0 2px 2px 0;
+  border-radius: 0;
+  display: inline-block;
+  padding: 3px;
+}
+
+.daterangepicker .calendar-table .next span {
+  transform: rotate(-45deg);
+  -webkit-transform: rotate(-45deg);
+}
+
+.daterangepicker .calendar-table .prev span {
+  transform: rotate(135deg);
+  -webkit-transform: rotate(135deg);
+}
+
+.daterangepicker .calendar-table th, .daterangepicker .calendar-table td {
+  white-space: nowrap;
+  text-align: center;
+  vertical-align: middle;
+  min-width: 32px;
+  width: 32px;
+  height: 24px;
+  line-height: 24px;
+  font-size: 12px;
+  border-radius: 4px;
+  border: 1px solid transparent;
+  white-space: nowrap;
+  cursor: pointer;
+}
+
+.daterangepicker .calendar-table {
+  border: 1px solid #fff;
+  border-radius: 4px;
+  background-color: #fff;
+}
+
+.daterangepicker .calendar-table table {
+  width: 100%;
+  margin: 0;
+  border-spacing: 0;
+  border-collapse: collapse;
+}
+
+.daterangepicker td.available:hover, .daterangepicker th.available:hover {
+  background-color: #eee;
+  border-color: transparent;
+  color: inherit;
+}
+
+.daterangepicker td.week, .daterangepicker th.week {
+  font-size: 80%;
+  color: #ccc;
+}
+
+.daterangepicker td.off, .daterangepicker td.off.in-range, .daterangepicker td.off.start-date, .daterangepicker td.off.end-date {
+  background-color: #fff;
+  border-color: transparent;
+  color: #999;
+}
+
+.daterangepicker td.in-range {
+  background-color: #ebf4f8;
+  border-color: transparent;
+  color: #000;
+  border-radius: 0;
+}
+
+.daterangepicker td.start-date {
+  border-radius: 4px 0 0 4px;
+}
+
+.daterangepicker td.end-date {
+  border-radius: 0 4px 4px 0;
+}
+
+.daterangepicker td.start-date.end-date {
+  border-radius: 4px;
+}
+
+.daterangepicker td.active, .daterangepicker td.active:hover {
+  background-color: #357ebd;
+  border-color: transparent;
+  color: #fff;
+}
+
+.daterangepicker th.month {
+  width: auto;
+}
+
+.daterangepicker td.disabled, .daterangepicker option.disabled {
+  color: #999;
+  cursor: not-allowed;
+  text-decoration: line-through;
+}
+
+.daterangepicker select.monthselect, .daterangepicker select.yearselect {
+  font-size: 12px;
+  padding: 1px;
+  height: auto;
+  margin: 0;
+  cursor: default;
+}
+
+.daterangepicker select.monthselect {
+  margin-right: 2%;
+  width: 56%;
+}
+
+.daterangepicker select.yearselect {
+  width: 40%;
+}
+
+.daterangepicker select.hourselect, .daterangepicker select.minuteselect, .daterangepicker select.secondselect, .daterangepicker select.ampmselect {
+  width: 50px;
+  margin: 0 auto;
+  background: #eee;
+  border: 1px solid #eee;
+  padding: 2px;
+  outline: 0;
+  font-size: 12px;
+}
+
+.daterangepicker .calendar-time {
+  text-align: center;
+  margin: 4px auto 0 auto;
+  line-height: 30px;
+  position: relative;
+}
+
+.daterangepicker .calendar-time select.disabled {
+  color: #ccc;
+  cursor: not-allowed;
+}
+
+.daterangepicker .drp-buttons {
+  clear: both;
+  text-align: right;
+  padding: 8px;
+  border-top: 1px solid #ddd;
+  display: none;
+  line-height: 12px;
+  vertical-align: middle;
+}
+
+.daterangepicker .drp-selected {
+  display: inline-block;
+  font-size: 12px;
+  padding-right: 8px;
+}
+
+.daterangepicker .drp-buttons .btn {
+  margin-left: 8px;
+  font-size: 12px;
+  font-weight: bold;
+  padding: 4px 8px;
+}
+
+.daterangepicker.show-ranges.single.rtl .drp-calendar.left {
+  border-right: 1px solid #ddd;
+}
+
+.daterangepicker.show-ranges.single.ltr .drp-calendar.left {
+  border-left: 1px solid #ddd;
+}
+
+.daterangepicker.show-ranges.rtl .drp-calendar.right {
+  border-right: 1px solid #ddd;
+}
+
+.daterangepicker.show-ranges.ltr .drp-calendar.left {
+  border-left: 1px solid #ddd;
+}
+
+.daterangepicker .ranges {
+  float: none;
+  text-align: left;
+  margin: 0;
+}
+
+.daterangepicker.show-calendar .ranges {
+  margin-top: 8px;
+}
+
+.daterangepicker .ranges ul {
+  list-style: none;
+  margin: 0 auto;
+  padding: 0;
+  width: 100%;
+}
+
+.daterangepicker .ranges li {
+  font-size: 12px;
+  padding: 8px 12px;
+  cursor: pointer;
+}
+
+.daterangepicker .ranges li:hover {
+  background-color: #eee;
+}
+
+.daterangepicker .ranges li.active {
+  background-color: #08c;
+  color: #fff;
+}
+
+/*  Larger Screen Styling */
+@media (min-width: 564px) {
+  .daterangepicker {
+    width: auto;
+  }
+
+  .daterangepicker .ranges ul {
+    width: 140px;
+  }
+
+  .daterangepicker.single .ranges ul {
+    width: 100%;
+  }
+
+  .daterangepicker.single .drp-calendar.left {
+    clear: none;
+  }
+
+  .daterangepicker.single .ranges, .daterangepicker.single .drp-calendar {
+    float: left;
+  }
+
+  .daterangepicker {
+    direction: ltr;
+    text-align: left;
+  }
+
+  .daterangepicker .drp-calendar.left {
+    clear: left;
+    margin-right: 0;
+  }
+
+  .daterangepicker .drp-calendar.left .calendar-table {
+    border-right: none;
+    border-top-right-radius: 0;
+    border-bottom-right-radius: 0;
+  }
+
+  .daterangepicker .drp-calendar.right {
+    margin-left: 0;
+  }
+
+  .daterangepicker .drp-calendar.right .calendar-table {
+    border-left: none;
+    border-top-left-radius: 0;
+    border-bottom-left-radius: 0;
+  }
+
+  .daterangepicker .drp-calendar.left .calendar-table {
+    padding-right: 8px;
+  }
+
+  .daterangepicker .ranges, .daterangepicker .drp-calendar {
+    float: left;
+  }
+}
+
+@media (min-width: 730px) {
+  .daterangepicker .ranges {
+    width: auto;
+  }
+
+  .daterangepicker .ranges {
+    float: left;
+  }
+
+  .daterangepicker.rtl .ranges {
+    float: right;
+  }
+
+  .daterangepicker .drp-calendar.left {
+    clear: none !important;
+  }
+}

Datei-Diff unterdrückt, da er zu groß ist
+ 6 - 0
static/vendor/daterangepicker/daterangepicker.min.js


+ 8 - 0
templates/webadmin/base.html

@@ -137,6 +137,14 @@ along with this program.  If not, see <https://www.gnu.org/licenses/>.
             </li>
             {{end}}
 
+            {{ if and .HasSearcher (.LoggedAdmin.HasPermission "view_events")}}
+            <li class="nav-item {{if eq .CurrentURL .EventsURL}}active{{end}}">
+                <a class="nav-link" href="{{.EventsURL}}">
+                    <i class="fas fa-clipboard-list"></i>
+                    <span>{{.EventsTitle}}</span></a>
+            </li>
+            {{end}}
+
             {{ if .LoggedAdmin.HasPermission "manage_system"}}
             <li class="nav-item {{if eq .CurrentURL .MaintenanceURL}}active{{end}}">
                 <a class="nav-link" href="{{.MaintenanceURL}}">

+ 607 - 0
templates/webadmin/events.html

@@ -0,0 +1,607 @@
+<!--
+Copyright (C) 2019-2022  Nicola Murino
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published
+by the Free Software Foundation, version 3.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <https://www.gnu.org/licenses/>.
+-->
+{{template "base" .}}
+
+{{define "title"}}{{.Title}}{{end}}
+
+{{define "extra_css"}}
+<link href="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet">
+<link href="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet">
+<link href="{{.StaticURL}}/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet">
+<link href="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet">
+<link href="{{.StaticURL}}/vendor/datatables/select.bootstrap4.min.css" rel="stylesheet">
+<link href="{{.StaticURL}}/vendor/bootstrap-select/css/bootstrap-select.min.css" rel="stylesheet">
+<link href="{{.StaticURL}}/vendor/daterangepicker/daterangepicker.css" rel="stylesheet">
+{{end}}
+
+{{define "page_body"}}
+<div id="errorMsg" class="card mb-4 border-left-warning" style="display: none;">
+    <div id="errorTxt" class="card-body text-form-error"></div>
+</div>
+<div class="card shadow mb-4">
+    <div class="card-header py-3">
+        <h6 class="m-0 font-weight-bold text-primary">Search logs</h6>
+    </div>
+    <div class="card-body">
+        <div class="form-row">
+            <div class="form-group col-md-3">
+                <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>
+                </select>
+            </div>
+            <div class="form-group col-md-3">
+                <select class="form-control selectpicker" id="idActions" name="actions" title="Actions" multiple>
+                </select>
+            </div>
+            <div class="form-group col-md-3">
+                <input type="text" class="form-control" id="idUsername" name="username" placeholder="Username">
+            </div>
+            <div class="form-group col-md-3">
+                <input type="text" class="form-control" id="idIp" name="ip" placeholder="IP address">
+            </div>
+        </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>
+                    <option value="SFTP">SFTP</option>
+                    <option value="SCP">SCP</option>
+                    <option value="SSH">SSH</option>
+                    <option value="FTP">FTP</option>
+                    <option value="DAV">DAV</option>
+                    <option value="HTTP">HTTP</option>
+                    <option value="OIDC">OIDC</option>
+                    <option value="HTTPShare">HTTPShare</option>
+                    <option value="DataRetention">DataRetention</option>
+                    <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">
+                    <div class="input-group-append">
+                        <button id="search-button" class="btn btn-primary" type="button" onclick="onSearchClicked()">
+                            <i class="fas fa-search fa-sm"></i>
+                        </button>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <div class="form-row">
+            <div class="form-group col-md-12">
+                <button class="btn btn-secondary mt-1 mb-1 px-5" type="button" onclick="onExportClicked()">Export</button>
+            </div>
+        </div>
+
+        <div class="table-responsive fs-events">
+            <table class="table table-hover nowrap" id="dataTableFs" width="100%" cellspacing="0">
+                <thead>
+                    <tr>
+                        <th>ID</th>
+                        <th>Time</th>
+                        <th>Action</th>
+                        <th>Path</th>
+                        <th>User</th>
+                        <th>Proto</th>
+                        <th>IP</th>
+                        <th>Info</th>
+                    </tr>
+                </thead>
+            </table>
+        </div>
+
+        <div class="table-responsive provider-events">
+            <table class="table table-hover nowrap" id="dataTableProvider" width="100%" cellspacing="0">
+                <thead>
+                    <tr>
+                        <th>ID</th>
+                        <th>Time</th>
+                        <th>Action</th>
+                        <th>Object</th>
+                        <th>User</th>
+                        <th>IP</th>
+                    </tr>
+                </thead>
+            </table>
+        </div>
+
+        <div id="paginationContainer" class="m-4 d-none">
+            <nav aria-label="Pagination">
+                <ul class="pagination justify-content-end">
+                    <li id="pageItemPrev" class="page-item disabled"><a id="pagePrevious" class="page-link" href="#" onclick="prevClicked()">Previous</a></li>
+                    <li id="pageItemNext" class="page-item disabled"><a id="pageNext" class="page-link" href="#" onclick="nextClicked()">Next</a></li>
+                </ul>
+            </nav>
+        </div>
+
+    </div>
+</div>
+{{end}}
+
+{{define "extra_js"}}
+<script src="{{.StaticURL}}/vendor/datatables/jquery.dataTables.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/dataTables.buttons.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/buttons.colVis.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/dataTables.fixedHeader.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/dataTables.responsive.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/dataTables.select.min.js"></script>
+<script src="{{.StaticURL}}/vendor/datatables/ellipsis.js"></script>
+<script src="{{.StaticURL}}/vendor/bootstrap-select/js/bootstrap-select.min.js"></script>
+<script src="{{.StaticURL}}/vendor/moment/js/moment.min.js"></script>
+<script src="{{.StaticURL}}/vendor/daterangepicker/daterangepicker.min.js"></script>
+<script type="text/javascript">
+    let dateFn = $.fn.dataTable.render.datetime();
+    let isFsDataTableInitialized = false;
+    let isProviderDataTableInitialized = false;
+    let pageSize = 20;
+    const paginationData = new Map();
+
+    function fileSizeIEC(a,b,c,d,e){
+        return (b=Math,c=b.log,d=1024,e=c(a)/c(d)|0,a/b.pow(d,e)).toFixed(1)
+            +' '+(e?'KMGTPEZY'[--e]+'iB':'Bytes')
+    }
+
+    function resetPagination() {
+        $('#pageItemPrev').addClass("disabled");
+        $('#pageItemNext').addClass("disabled");
+        $('#paginationContainer').addClass("d-none");
+        paginationData.delete("firstId");
+        paginationData.delete("firstTs");
+        paginationData.delete("lastId");
+        paginationData.delete("lastTs");
+        paginationData.set("prevClicked",false);
+        paginationData.set("nextClicked",false);
+    }
+
+    function prevClicked(){
+        paginationData.set("prevClicked",true);
+        paginationData.set("nextClicked",false);
+        doSearch();
+    }
+
+    function nextClicked(){
+        paginationData.set("prevClicked",false);
+        paginationData.set("nextClicked",true);
+        doSearch();
+    }
+
+    function handleResponseData(data) {
+        let length = data.length;
+        let isNext = paginationData.get("nextClicked");
+        let isPrev = paginationData.get("prevClicked");
+
+        if (length > pageSize) {
+            data.pop();
+            length--;
+            if (isPrev || isNext){
+                $('#pageItemPrev').removeClass("disabled");
+            }
+            $('#pageItemNext').removeClass("disabled");
+        } else {
+            if (isPrev){
+                $('#pageItemPrev').addClass("disabled");
+                $('#pageItemNext').removeClass("disabled");
+            } else if (isNext){
+                $('#pageItemPrev').removeClass("disabled");
+                $('#pageItemNext').addClass("disabled");
+            } else {
+                $('#pageItemNext').addClass("disabled");
+            }
+        }
+        if (isPrev){
+            data = data.reverse();
+        }
+        if (length > 0){
+            paginationData.set("lastId",data[0].id);
+            paginationData.set("lastTs",data[0].timestamp);
+            paginationData.set("firstId",data[length-1].id);
+            paginationData.set("firstTs",data[length-1].timestamp);
+            $('#paginationContainer').removeClass("d-none");
+        } else {
+            resetPagination();
+        }
+        return data;
+    }
+
+    function onExportClicked() {
+        paginationData.set("prevClicked",false);
+        paginationData.set("nextClicked",false);
+        let exportURL = getSearchURL(true);
+        var ts = new Date().getTime().toString();
+        window.open(`${exportURL}&_=${ts}`);
+    }
+
+    function onSearchClicked() {
+        resetPagination();
+        doSearch();
+    }
+
+    function doSearch() {
+        let eventType = $('#idEventType').val();
+        let table;
+        if (eventType == 1){
+            if (!isFsDataTableInitialized){
+                initFsDatatable();
+                return;
+            }
+            table = $('#dataTableFs').DataTable();
+        } else {
+            if (!isProviderDataTableInitialized){
+                initProviderDatatable();
+                return;
+            }
+            table = $('#dataTableProvider').DataTable();
+        }
+        table.clear().draw();
+        table.ajax.url(getSearchURL(false)).load();
+    }
+
+    function getSearchURL(csvExport) {
+        let url = "";
+        let eventType = $('#idEventType').val();
+        let order = "DESC";
+        let limit = pageSize + 1;
+        if (csvExport){
+            order = "ASC";
+        }
+        if (eventType == 1){
+            url = "{{.FsEventsSearchURL}}?limit="+limit;
+            let protocols = [];
+            $('#idProtocols').find('option:selected').each(function(){
+                protocols.push($(this).val());
+            });
+            if (protocols.length > 0){
+                url+="&protocols="+encodeURIComponent(String(protocols));
+            }
+            let statuses = [];
+            $('#idStatuses').find('option:selected').each(function(){
+                statuses.push($(this).val());
+            });
+            if (statuses.length > 0){
+                url+="&statuses="+encodeURIComponent(String(statuses));
+            }
+        } else {
+            url = "{{.ProviderEventsSearchURL}}?limit="+limit;
+        }
+        let actions = [];
+        $('#idActions').find('option:selected').each(function(){
+            actions.push($(this).val());
+        });
+        if (actions.length > 0){
+            url+="&actions="+encodeURIComponent(String(actions));
+        }
+        let username = $('#idUsername').val();
+        if (username){
+            url+="&username="+encodeURIComponent(username);
+        }
+        let ip = $('#idIp').val();
+        if (ip){
+            url+="&ip="+encodeURIComponent(ip);
+        }
+        let drp = $('#dateTimeRange').data('daterangepicker');
+        let excludeIds = [];
+        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"));
+        } 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"));
+        } 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));
+        }
+        url+="&order="+order;
+        if (csvExport){
+            url+="&csv_export=true";
+        }
+        return url;
+    }
+
+    function initProviderDatatable(){
+        let tableProvider = $('#dataTableProvider').DataTable({
+            "ajax": {
+                "url": getSearchURL(false),
+                "dataSrc": handleResponseData,
+                "error": function ($xhr, textStatus, errorThrown) {
+                    $(".dataTables_processing").hide();
+                    var txt = "Failed to get provider events";
+                    if ($xhr) {
+                        var json = $xhr.responseJSON;
+                        if (json) {
+                            if (json.message){
+                                txt += ": " + json.message;
+                            } else {
+                                txt += ": " + json.error;
+                            }
+                        }
+                    }
+                    $('#errorTxt').text(txt);
+                    $('#errorMsg').show();
+                    setTimeout(function () {
+                        $('#errorMsg').hide();
+                    }, 10000);
+                }
+            },
+            "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": "action"
+                },
+                {
+                    "data": "object_type",
+                    "render": function (data, type, row) {
+                        if (type === 'display') {
+                            let ellipsisFn = $.fn.dataTable.render.ellipsis(70, true);
+                            return ellipsisFn(`${data}: ${row["object_name"]}`,type);
+                        }
+                        return data;
+                    }
+                },
+                {
+                    "data": "username",
+                    "defaultContent": ""
+                },
+                {
+                    "data": "ip",
+                    "defaultContent": ""
+                }
+            ],
+            "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(tableProvider);
+
+        isProviderDataTableInitialized = true;
+    }
+
+    function initFsDatatable(){
+        let tableFs = $('#dataTableFs').DataTable({
+            "ajax": {
+                "url": getSearchURL(false),
+                "dataSrc": handleResponseData,
+                "error": function ($xhr, textStatus, errorThrown) {
+                    $(".dataTables_processing").hide();
+                    var txt = "Failed to get filesystem events";
+                    if ($xhr) {
+                        let json = $xhr.responseJSON;
+                        if (json) {
+                            if (json.message){
+                                txt += ": " + json.message;
+                            } else {
+                                txt += ": " + json.error;
+                            }
+                        }
+                    }
+                    $('#errorTxt').text(txt);
+                    $('#errorMsg').show();
+                    setTimeout(function () {
+                        $('#errorMsg').hide();
+                    }, 10000);
+                }
+            },
+            "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": "action"
+                },
+                {
+                    "data": "virtual_path",
+                    "render": function (data, type, row) {
+                        if (type === 'display') {
+                            let ellipsisFn = $.fn.dataTable.render.ellipsis(70, true);
+                            if (row["virtual_target_path"]){
+                                return ellipsisFn(`${data} => ${row["virtual_target_path"]}`,type);
+                            }
+                            return ellipsisFn(data,type);
+                        }
+                        return data;
+                    }
+                },
+                {
+                    "data": "username",
+                    "defaultContent": ""
+                },
+                {
+                    "data": "protocol",
+                    "defaultContent": ""
+                },
+                {
+                    "data": "ip",
+                    "defaultContent": ""
+                },
+                {
+                    "data": "status",
+                    "render": function (data, type, row) {
+                        if (type === 'display') {
+                            let info = 'OK';
+                            if (data == 2){
+                                info = 'KO';
+                            } else if (data == 3){
+                                info = 'Quota exceeded'
+                            }
+                            if (row["file_size"]){
+                                let humanSize = fileSizeIEC(row["file_size"]);
+                                info+=`. ${humanSize}`
+                            }
+                            if (row["ssh_cmd"]){
+                                info+=`. Command: ${row["ssh_cmd"]}`
+                            }
+                            return info;
+                        }
+                        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(tableFs);
+        isFsDataTableInitialized = true;
+    }
+
+    function selectFsEvents(){
+        let idActions = $('#idActions');
+        idActions.selectpicker('deselectAll');
+        idActions.find('option').remove();
+        idActions.find('li').remove();
+        idActions.append($('<option>').val('upload').text('Upload'));
+        idActions.append($('<option>').val('download').text('Download'));
+        idActions.append($('<option>').val('mkdir').text('Mkdir'));
+        idActions.append($('<option>').val('rmdir').text('Rmdir'));
+        idActions.append($('<option>').val('rename').text('Rename'));
+        idActions.append($('<option>').val('delete').text('Delete'));
+        idActions.append($('<option>').val('first-upload').text('First upload'));
+        idActions.append($('<option>').val('first-download').text('First download'));
+        idActions.append($('<option>').val('pre-upload').text('Pre-upload'));
+        idActions.append($('<option>').val('pre-download').text('Pre-download'));
+        idActions.append($('<option>').val('pre-delete').text('Pre-delete'));
+        idActions.append($('<option>').val('ssh_cmd').text('SSH command'));
+        idActions.selectpicker('refresh');
+
+        $('#idUsername').val("");
+        $('#idIp').val("");
+        $('.provider-events').hide();
+        $('.fs-events').show();
+        onSearchClicked();
+    }
+
+    function selectProviderEvents(){
+        let idActions = $('#idActions');
+        idActions.selectpicker('deselectAll');
+        idActions.find('option').remove();
+        idActions.find('li').remove();
+        idActions.append($('<option>').val('add').text('Add'));
+        idActions.append($('<option>').val('update').text('Update'));
+        idActions.append($('<option>').val('delete').text('Delete'));
+        idActions.selectpicker('refresh');
+
+        $('#idUsername').val("");
+        $('#idIp').val("");
+        $('.fs-events').hide();
+        $('.provider-events').show();
+        onSearchClicked();
+    }
+
+    function onEventChanged(val){
+        switch (val){
+            case '1':
+            case 1:
+                selectFsEvents();
+                break;
+            case '2':
+            case 2:
+                selectProviderEvents();
+                break;
+            default:
+                console.log(`unsupported event type: ${val}`);
+        }
+    }
+
+    $(document).ready(function () {
+        $('#dateTimeRange').daterangepicker({
+            timePicker: true,
+            timePicker24Hour: true,
+            opens: 'left',
+            startDate: moment().add(-1,'hour'),
+            endDate: moment(),
+            locale: {
+                format: 'DD/MM HH:mm'
+            }
+        });
+
+        $.fn.dataTable.ext.errMode = 'none';
+
+        onEventChanged('1');
+    });
+</script>
+{{end}}

+ 4 - 4
templates/webadmin/folders.html

@@ -243,7 +243,8 @@ function deleteAction() {
             enabled: false
         };
 
-        var table = $('#dataTable').DataTable({
+        let dateFn = $.fn.dataTable.render.datetime();
+        let table = $('#dataTable').DataTable({
             "select": {
                 "style": "single",
                 "blurable": true
@@ -296,11 +297,10 @@ function deleteAction() {
 			                return data;
 		                }
                         if (row[0] !== ""){
-                            var dateFn = $.fn.dataTable.render.datetime();
-                            var formattedDate = dateFn(row[0], type);
+                            let formattedDate = dateFn(row[0], type);
                             data = `${data}. Updated at: ${formattedDate}`;
                         }
-                        var ellipsisFn = $.fn.dataTable.render.ellipsis(60, true);
+                        let ellipsisFn = $.fn.dataTable.render.ellipsis(60, true);
                         return ellipsisFn(data, type);
                     }
                 }

+ 3 - 3
templates/webadmin/users.html

@@ -263,7 +263,8 @@ along with this program.  If not, see <https://www.gnu.org/licenses/>.
             enabled: false
         };
 
-        var table = $('#dataTable').DataTable({
+        let dateFn = $.fn.dataTable.render.datetime();
+        let table = $('#dataTable').DataTable({
             "select": {
                 "style": "single",
                 "blurable": true
@@ -318,11 +319,10 @@ along with this program.  If not, see <https://www.gnu.org/licenses/>.
 			                return data;
 		                }
                         if (row[12] !== ""){
-                            var dateFn = $.fn.dataTable.render.datetime();
                             var formattedDate = dateFn(row[12], type);
                             data = `${data}. Updated at: ${formattedDate}`;
                         }
-                        var ellipsisFn = $.fn.dataTable.render.ellipsis(70, true);
+                        let ellipsisFn = $.fn.dataTable.render.ellipsis(70, true);
                         return ellipsisFn(data, type);
                     }
                 },

+ 2 - 2
templates/webclient/files.html

@@ -953,7 +953,7 @@ along with this program.  If not, see <https://www.gnu.org/licenses/>.
                 var downloadURL = '{{.DownloadURL}}';
                 var currentDir = '{{.CurrentDir}}';
                 var ts = new Date().getTime().toString();
-                window.location = `${downloadURL}?path=${currentDir}&files=${files}&_=${ts}`;
+                window.open(`${downloadURL}?path=${currentDir}&files=${files}&_=${ts}`);
             },
             enabled: false
         };
@@ -1019,7 +1019,7 @@ along with this program.  If not, see <https://www.gnu.org/licenses/>.
                 var shareURL = '{{.ShareURL}}';
                 var currentDir = '{{.CurrentDir}}';
                 var ts = new Date().getTime().toString();
-                window.location = `${shareURL}?path=${currentDir}&files=${files}&_=${ts}`;
+                window.open(`${shareURL}?path=${currentDir}&files=${files}&_=${ts}`,'_blank');
             },
             enabled: false
         };

+ 1 - 1
templates/webclient/sharefiles.html

@@ -410,7 +410,7 @@ along with this program.  If not, see <https://www.gnu.org/licenses/>.
                 var downloadURL = '{{.DownloadURL}}';
                 var currentDir = '{{.CurrentDir}}';
                 var ts = new Date().getTime().toString();
-                window.location = `${downloadURL}?path=${currentDir}&files=${files}&_=${ts}`;
+                window.open(`${downloadURL}?path=${currentDir}&files=${files}&_=${ts}`);
             },
             enabled: false
         };

+ 6 - 6
tests/eventsearcher/go.mod

@@ -4,23 +4,23 @@ go 1.19
 
 require (
 	github.com/hashicorp/go-plugin v1.4.6
-	github.com/sftpgo/sdk v0.1.3-0.20221203095324-2feef3600930
+	github.com/sftpgo/sdk v0.1.3-0.20221205110949-c15308d4236e
 )
 
 require (
 	github.com/fatih/color v1.13.0 // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/google/go-cmp v0.5.9 // indirect
-	github.com/hashicorp/go-hclog v1.3.1 // indirect
+	github.com/hashicorp/go-hclog v1.4.0 // indirect
 	github.com/hashicorp/yamux v0.1.1 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.16 // indirect
 	github.com/mitchellh/go-testing-interface v1.14.1 // indirect
 	github.com/oklog/run v1.1.0 // indirect
-	golang.org/x/net v0.2.0 // indirect
-	golang.org/x/sys v0.2.0 // indirect
-	golang.org/x/text v0.4.0 // indirect
-	google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd // indirect
+	golang.org/x/net v0.4.0 // indirect
+	golang.org/x/sys v0.3.0 // indirect
+	golang.org/x/text v0.5.0 // indirect
+	google.golang.org/genproto v0.0.0-20221206210731-b1a01be3a5f6 // indirect
 	google.golang.org/grpc v1.51.0 // indirect
 	google.golang.org/protobuf v1.28.1 // indirect
 )

+ 12 - 12
tests/eventsearcher/go.sum

@@ -9,8 +9,8 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/hashicorp/go-hclog v1.3.1 h1:vDwF1DFNZhntP4DAjuTpOw3uEgMUpXh1pB5fW9DqHpo=
-github.com/hashicorp/go-hclog v1.3.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
+github.com/hashicorp/go-hclog v1.4.0 h1:ctuWFGrhFha8BnnzxqeRGidlEcQkDyL5u8J8t5eA11I=
+github.com/hashicorp/go-hclog v1.4.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
 github.com/hashicorp/go-plugin v1.4.6 h1:MDV3UrKQBM3du3G7MApDGvOsMYy3JQJ4exhSoKBAeVA=
 github.com/hashicorp/go-plugin v1.4.6/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s=
 github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
@@ -30,26 +30,26 @@ 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-0.20221203095324-2feef3600930 h1:znJ52fQBSAQhacaQvZAfkpTioqpcutDREM/H8NttKzU=
-github.com/sftpgo/sdk v0.1.3-0.20221203095324-2feef3600930/go.mod h1:S2S/Q9fgUpXmL11YoCCt0hyCkEwH1LzQM/6QVsbUCFg=
+github.com/sftpgo/sdk v0.1.3-0.20221205110949-c15308d4236e h1:F3G/BReUSU8TX6Kmk0moQgQAk9Ouiv2I+pg//o1IR6U=
+github.com/sftpgo/sdk v0.1.3-0.20221205110949-c15308d4236e/go.mod h1:S2S/Q9fgUpXmL11YoCCt0hyCkEwH1LzQM/6QVsbUCFg=
 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.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
-golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
+golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
+golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
 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=
 golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 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.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
-golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
-golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
+golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
+golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd h1:OjndDrsik+Gt+e6fs45z9AxiewiKyLKYpA45W5Kpkks=
-google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE=
+google.golang.org/genproto v0.0.0-20221206210731-b1a01be3a5f6 h1:AGXp12e/9rItf6/4QymU7WsAUwCf+ICW75cuR91nJIc=
+google.golang.org/genproto v0.0.0-20221206210731-b1a01be3a5f6/go.mod h1:1dOng4TWOomJrDGhpXjfCD35wQC6jnC7HpRmOFRqEV0=
 google.golang.org/grpc v1.51.0 h1:E1eGv1FTqoLIdnBCZufiSHgKjlqG6fKFf6pPWtMTh8U=
 google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=

+ 6 - 6
tests/ipfilter/go.mod

@@ -4,22 +4,22 @@ go 1.19
 
 require (
 	github.com/hashicorp/go-plugin v1.4.6
-	github.com/sftpgo/sdk v0.1.2
+	github.com/sftpgo/sdk v0.1.3-0.20221205110949-c15308d4236e
 )
 
 require (
 	github.com/fatih/color v1.13.0 // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
-	github.com/hashicorp/go-hclog v1.3.1 // indirect
+	github.com/hashicorp/go-hclog v1.4.0 // indirect
 	github.com/hashicorp/yamux v0.1.1 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.16 // indirect
 	github.com/mitchellh/go-testing-interface v1.14.1 // indirect
 	github.com/oklog/run v1.1.0 // indirect
-	golang.org/x/net v0.2.0 // indirect
-	golang.org/x/sys v0.2.0 // indirect
-	golang.org/x/text v0.4.0 // indirect
-	google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd // indirect
+	golang.org/x/net v0.4.0 // indirect
+	golang.org/x/sys v0.3.0 // indirect
+	golang.org/x/text v0.5.0 // indirect
+	google.golang.org/genproto v0.0.0-20221206210731-b1a01be3a5f6 // indirect
 	google.golang.org/grpc v1.51.0 // indirect
 	google.golang.org/protobuf v1.28.1 // indirect
 )

+ 12 - 12
tests/ipfilter/go.sum

@@ -8,8 +8,8 @@ github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
-github.com/hashicorp/go-hclog v1.3.1 h1:vDwF1DFNZhntP4DAjuTpOw3uEgMUpXh1pB5fW9DqHpo=
-github.com/hashicorp/go-hclog v1.3.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
+github.com/hashicorp/go-hclog v1.4.0 h1:ctuWFGrhFha8BnnzxqeRGidlEcQkDyL5u8J8t5eA11I=
+github.com/hashicorp/go-hclog v1.4.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
 github.com/hashicorp/go-plugin v1.4.6 h1:MDV3UrKQBM3du3G7MApDGvOsMYy3JQJ4exhSoKBAeVA=
 github.com/hashicorp/go-plugin v1.4.6/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s=
 github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
@@ -29,27 +29,27 @@ 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.2 h1:j4V63RuVcYfJAOWV0zRUofa1PlQvKU2ujly0lB7quVA=
-github.com/sftpgo/sdk v0.1.2/go.mod h1:PTp1TfXa+95wHw9yuZu7BA3vmzLqbRkz3gBmMNnwFQg=
+github.com/sftpgo/sdk v0.1.3-0.20221205110949-c15308d4236e h1:F3G/BReUSU8TX6Kmk0moQgQAk9Ouiv2I+pg//o1IR6U=
+github.com/sftpgo/sdk v0.1.3-0.20221205110949-c15308d4236e/go.mod h1:S2S/Q9fgUpXmL11YoCCt0hyCkEwH1LzQM/6QVsbUCFg=
 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.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
-golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
+golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
+golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
 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=
 golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 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.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
-golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
-golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
+golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
+golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
-google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd h1:OjndDrsik+Gt+e6fs45z9AxiewiKyLKYpA45W5Kpkks=
-google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE=
+google.golang.org/genproto v0.0.0-20221206210731-b1a01be3a5f6 h1:AGXp12e/9rItf6/4QymU7WsAUwCf+ICW75cuR91nJIc=
+google.golang.org/genproto v0.0.0-20221206210731-b1a01be3a5f6/go.mod h1:1dOng4TWOomJrDGhpXjfCD35wQC6jnC7HpRmOFRqEV0=
 google.golang.org/grpc v1.51.0 h1:E1eGv1FTqoLIdnBCZufiSHgKjlqG6fKFf6pPWtMTh8U=
 google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.