Compare commits

...

49 Commits

Author SHA1 Message Date
c0aa73c1bf
Add govulncheck to tools 2025-03-20 19:19:20 +01:00
078c5fc785
Add gomajor to tools 2025-03-20 19:16:37 +01:00
73c8486fc8
flake.lock: Update
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/d69ab0d71b22fa1ce3dbeff666e6deb4917db049?narHash=sha256-k7VSqvv0r1r53nUI/IfPHCppkUAddeXn843YlAC5DR0%3D' (2025-03-05)
  → 'github:NixOS/nixpkgs/b6eaf97c6960d97350c584de1b6dcff03c9daf42?narHash=sha256-Txwa5uO%2BqpQXrNG4eumPSD%2BhHzzYi/CdaM80M9XRLCo%3D' (2025-03-18)
2025-03-20 19:12:46 +01:00
ed4d970723
Make metrics endpoint configurable 2025-03-20 18:57:31 +01:00
9b54cec29c
Update cli to v3 2025-03-20 18:53:23 +01:00
1230fb52d7
Update go-toml 2025-03-20 18:40:50 +01:00
cd0d5c469f
Update pgx 2025-03-20 18:39:18 +01:00
398160e6eb Merge pull request 'Remove unused code' (#12) from remove-old-crust into master
Reviewed-on: #12
2025-03-20 17:35:22 +00:00
3a5fa290e2
Remove unused code 2025-03-20 18:34:28 +01:00
e4327cd3a8
Fix table text alignment 2025-03-19 23:21:26 +01:00
cdbcf0e03b
Merge branch 'remove-zap' 2025-03-19 23:18:06 +01:00
46d9f4d64a
Replace zap with slog 2025-03-19 23:16:38 +01:00
49553fa965
Update go deps 2025-03-19 22:58:47 +01:00
aa5b9588d1
Remove build requirements from README 2025-03-19 22:40:10 +01:00
858c11b853
Minor README improvements 2025-03-19 22:39:04 +01:00
85fd24180a
Remove deprecated features from config example 2025-03-19 22:36:46 +01:00
b2e3632b56
Update README 2025-03-19 22:35:33 +01:00
73dce3d1f3
Update Dockerfile 2025-03-19 22:28:28 +01:00
d86bc2be07
Remove Makefile 2025-03-19 22:08:45 +01:00
39a682fd41 Merge pull request 'Add flake.nix and rewrite frontend' (#5) from nixify into master
Reviewed-on: #5
2025-03-19 21:07:51 +00:00
a0e661eca3
Add tarball output to flake 2025-03-19 17:50:36 +01:00
db294c171d
Sort stats after retrieving from api 2025-03-19 17:07:16 +01:00
2c75603046
Relax stats update interval 2025-03-19 16:58:08 +01:00
a7b1ba584f
Update npm deps hash 2025-03-19 16:53:39 +01:00
89237c2b84
Improve frontend 2025-03-19 16:49:18 +01:00
5ade1c0593
Change frontend deps 2025-03-09 03:20:53 +01:00
ec6c0cc45b
Frontend fixes 2025-03-08 13:01:09 +01:00
9f9e0524f5
flake.lock: Update
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/ba487dbc9d04e0634c64e3b1f0d25839a0a68246?narHash=sha256-WZNlK/KX7Sni0RyqLSqLPbK8k08Kq7H7RijPJbq9KHM%3D' (2025-03-03)
  → 'github:NixOS/nixpkgs/d69ab0d71b22fa1ce3dbeff666e6deb4917db049?narHash=sha256-k7VSqvv0r1r53nUI/IfPHCppkUAddeXn843YlAC5DR0%3D' (2025-03-05)
2025-03-07 02:10:57 +01:00
13f126ccbc
Add rev to ldflags 2025-03-07 01:49:38 +01:00
80ac8148e7
Bump version 2025-03-07 01:34:12 +01:00
d7f0ba7486
Improve flake build process 2025-03-07 01:33:31 +01:00
5682777bfe
Nixify and start frontend update 2025-03-07 01:23:15 +01:00
4a3c022958 Merge pull request 'Remove old smtp and port stuff' (#4) from remove-smtp into master
Reviewed-on: #4
2023-10-12 19:07:24 +00:00
53ae82727c Remove old smtp and port stuff 2023-10-12 21:06:10 +02:00
fb2fb5de5e
fix duplicate setspeed call 2022-09-28 15:31:33 +02:00
ca96115fe2
Fix bug with throttlespeed in ssh conn 2022-09-28 12:45:55 +02:00
e9d2f3581c
Add debug log to ssh connection init 2022-09-28 12:36:16 +02:00
8084362d22
Improve logging 2022-09-28 12:28:15 +02:00
837f89e5d3
update dependencies 2022-09-28 12:17:46 +02:00
38ff8949b9
Tryout go test fuzzing 2022-09-01 03:59:18 +02:00
fce78d234d Merge pull request 'Add bbolt store' (#3) from feature/bbolt-db into master
Reviewed-on: #3
2022-08-31 21:25:27 +00:00
940a512d56
Add bbolt store 2022-08-31 23:22:20 +02:00
52c2bd1060
Add config to not log requests to metrics endpoint 2022-08-28 02:35:33 +02:00
d2fa727990
Update dependencies and fix some linter warnings 2022-08-28 02:23:36 +02:00
0ed71be705
Bump version 2022-08-26 12:23:31 +02:00
4ff47521b4 Merge pull request 'feature/use-channel-for-all' (#2) from feature/use-channel-for-all into master
Reviewed-on: #2
2022-08-26 10:22:58 +00:00
07fe16b72a
Change store All to use channel 2022-08-26 12:21:52 +02:00
4c77b5ef9b
Use unprivileged port for ports test 2022-08-26 12:21:35 +02:00
329d93a759 Fix Makefile 2022-02-09 15:18:34 +01:00
72 changed files with 10905 additions and 12427 deletions

4
.gitignore vendored
View File

@ -1,3 +1,5 @@
**/node_modules **/node_modules
**/dist **/dist
honeypot/Geoacumen-Country.mmdb honeypot/Geoacumen-Country.mmdb
result
frontend/.parcel-cache

View File

@ -1,12 +1,16 @@
FROM node:latest as frontend-builder FROM node:alpine as frontend-builder
RUN mkdir -p /app/frontend WORKDIR /app
COPY web/frontend/yarn.lock /app/frontend/yarn.lock COPY frontend/package.json /app
COPY web/frontend/package.json /app/frontend/package.json COPY frontend/package-lock.json /app
WORKDIR /app/frontend RUN npm install
RUN yarn install COPY frontend .
COPY web/frontend /app/frontend RUN npm run build
WORKDIR /app/frontend
RUN GENERATE_SOURCEMAP=false yarn build FROM alpine:latest as geoip-fetcher
RUN apk add --no-cache git
WORKDIR /app
RUN git clone https://github.com/geoacumen/geoacumen-country.git
RUN find .
FROM golang:latest as builder FROM golang:latest as builder
WORKDIR /app WORKDIR /app
@ -14,16 +18,15 @@ COPY go.mod /app/go.mod
COPY go.sum /app/go.sum COPY go.sum /app/go.sum
RUN go mod download RUN go mod download
COPY . /app COPY . /app
RUN rm -rf /app/server/frontend COPY --from=geoip-fetcher /app/geoacumen-country/Geoacumen-Country.mmdb honeypot/ssh
COPY --from=frontend-builder /app/frontend/dist /app/web/frontend/dist COPY --from=frontend-builder /app/dist /app/web/frontend/dist
RUN CGO_ENABLED=0 INSTALL_PREFIX=/app make install RUN CGO_ENABLED=0 go build -tags embed cmd/apiary/apiary.go
FROM alpine:latest FROM alpine:latest
RUN apk add --no-cache curl RUN apk add --no-cache curl
WORKDIR /app WORKDIR /app
COPY --from=builder /app/etc/apiary/apiary.toml /app/apiary.toml COPY --from=builder /app/apiary.toml /app/apiary.toml
COPY --from=builder /app/bin/apiary /app/apiary COPY --from=builder /app/apiary /app/apiary
EXPOSE 8080 EXPOSE 8080
EXPOSE 2222 EXPOSE 2222
HEALTHCHECK --interval=1m --timeout=10s --start-period=5s --retries=3 CMD curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8080/api/health || exit 1
CMD ["/app/apiary", "serve"] CMD ["/app/apiary", "serve"]

View File

@ -1,62 +0,0 @@
NAME = apiary
.DEFAULT_GOAL := all
.PHONY: all clean
INSTALL_PREFIX ?= /usr/local
VERSION = $(shell cat version.go |grep "var Version"| cut -d "=" -f2| tr -d "\" ")
ARCH = $(shell go env | grep GOHOSTARCH | cut -d"=" -f2 | tr -d "\"")
OS = $(shell go env | grep GOHOSTOS | cut -d"=" -f2 | tr -d "\"")
GIT_COMMIT := $(shell git rev-parse --short HEAD)
BUILD_DIR = build/binary/current
BUILD_OUTPUT = $(BUILD_DIR)/$(NAME)-$(VERSION)-$(OS)-$(ARCH)
BUILD_FLAGS = -tags embed -ldflags "-X git.t-juice.club/torjus/apiary.Build=$(GIT_COMMIT)" -o $(BUILD_OUTPUT)
GEODB_URL = https://raw.githubusercontent.com/geoacumen/geoacumen-country/master/Geoacumen-Country.mmdb
GEODB_PATH = honeypot/Geoacumen-Country.mmdb
FRONTEND_BUILD_DIR = web/frontend/dist
ifeq ($(INSTALL_PREFIX), /)
INSTALL_BIN_DIR=/usr/bin
INSTALL_ETC_DIR=/etc/$(NAME)
else
INSTALL_BIN_DIR=$(INSTALL_PREFIX)/bin
INSTALL_ETC_DIR=$(INSTALL_PREFIX)/etc/$(NAME)
endif
$(GEODB_PATH):
curl $(GEODB_URL) -o $(GEODB_PATH)
$(BUILD_DIR):
mkdir -p $(BUILD_DIR)
$(FRONTEND_BUILD_DIR):
cd web/frontend && yarn install && yarn build
$(BUILD_OUTPUT): $(BUILD_DIR) $(GEODB_PATH) $(FRONTEND_BUILD_DIR)
CGO_ENABLED=0 go build $(BUILD_FLAGS) cmd/apiary.go
frontend:
cd web/frontend && yarn install && yarn build
clean:
rm -rv $(BUILD_DIR)
rm -rv $(FRONTEND_BUILD_DIR)
geodb: $(GEODB_PATH)
build: $(BUILD_OUTPUT)
install: build
mkdir -p $(INSTALL_BIN_DIR)
mkdir -p $(INSTALL_ETC_DIR)
install -m 755 $(BUILD_OUTPUT) $(INSTALL_BIN_DIR)/$(NAME)
install -m 755 apiary.toml $(INSTALL_ETC_DIR)/apiary.toml
uninstall:
rm -v $(INSTALL_ETC_DIR)/apiary.toml
rmdir -v $(INSTALL_ETC_DIR)
rm -v $(INSTALL_BIN_DIR)/$(NAME)

View File

@ -2,34 +2,32 @@
SSH honeypot with web-frontend. SSH honeypot with web-frontend.
## TODO ## Build using nix
* Fix janky ass Makefile ```console
$ nix build .#
## Requirements $ ./result/bin/apiary --version
apiary version v0.2.1-d86bc2be079c6b18a9f941752ebad45d925366d1 (go1.23.6)
* `go >= 1.16`
Requires a postgres database if you want data to persist through server restart.
## Build
```text
# make frontend
# make build
``` ```
## Build without nix
See [Dockerfile](./Dockerfile) for how to build frontend and backend. Or use Dockerfile
and copy binary from image.
## Install ## Install
```text ```console
# Build and install Build or download binary
INSTALL_PREFIX=/ sudo make install $ cp ./apiary /usr/local/bin/apiary
# Edit config file Edit config file, should use postgres if you want persistent storage.
vim /etc/apiary/apiary.toml See example config for the different options.
$ vim /etc/apiary/apiary.toml
# Run $ /usr/bin/apiary serve
/usr/bin/apiary serve 2021-04-10T11:27:42.783+0200 INFO APP Starting SSH server
# 2021-04-10T11:27:42.783+0200 INFO APP Starting SSH server 2021-04-10T11:27:42.783+0200 INFO APP Starting web server
# 2021-04-10T11:27:42.783+0200 INFO APP Starting web server
``` ```

View File

@ -21,7 +21,7 @@ HostKeyPath = ""
# Log level for SSH Honeypot # Log level for SSH Honeypot
# Must be either "DEBUG", "INFO", "WARN", "ERROR", "FATAL", "NONE" # Must be either "DEBUG", "INFO", "WARN", "ERROR", "FATAL", "NONE"
# Default: "INFO" # Default: "INFO"
LogLevel = "INFO" LogLevel = "DEBUG"
# Address and port to listen to # Address and port to listen to
# Default: ":2222" # Default: ":2222"
ListenAddr = ":2222" ListenAddr = ":2222"
@ -35,9 +35,15 @@ ThrottleSpeed = 10240.0
# Must be either "DEBUG", "INFO", "WARN", "ERROR", "FATAL", "NONE" # Must be either "DEBUG", "INFO", "WARN", "ERROR", "FATAL", "NONE"
# Default: "INFO" # Default: "INFO"
LogLevel = "INFO" LogLevel = "INFO"
# Enable metrics endpoint
# Default: false
MetricsEnable = false
# Enable access logging # Enable access logging
# Default: true # Default: true
AccessLogEnable = true AccessLogEnable = true
# Disable logging of requests to metrics endpoint
# Default: false
AccessLogIgnoreMetrics = false
# Address and port to listen to # Address and port to listen to
# Default: ":8080" # Default: ":8080"
ListenAddr = ":8080" ListenAddr = ":8080"
@ -60,32 +66,3 @@ CacheDir = "/var/apiary/certs"
# Default: true # Default: true
RedirectHTTP = true RedirectHTTP = true
[Ports]
# Enable the port listener.
# Default: false
Enable = false
# Which address to listen on.
# Default: "" (listen to all addresses)
Addr = ""
# Which TCP ports to listen to.
# Default: []
TCPPorts = ["25"]
# Which UDP ports to listen to.
# Default: []
UDPPorts = ["25"]
[SMTP]
# Enable the port listener.
# Default: false
Enable = true
# Which address and port to listen on.
# Default: ":25"
Addr = ":25"
# Enable collecting prometheus metrics
# Default: false
EnableMetrics = true

View File

@ -3,6 +3,7 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"log/slog"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
@ -11,29 +12,19 @@ import (
"git.t-juice.club/torjus/apiary" "git.t-juice.club/torjus/apiary"
"git.t-juice.club/torjus/apiary/config" "git.t-juice.club/torjus/apiary/config"
"git.t-juice.club/torjus/apiary/honeypot/ports"
"git.t-juice.club/torjus/apiary/honeypot/smtp"
"git.t-juice.club/torjus/apiary/honeypot/ssh" "git.t-juice.club/torjus/apiary/honeypot/ssh"
"git.t-juice.club/torjus/apiary/honeypot/ssh/store" "git.t-juice.club/torjus/apiary/honeypot/ssh/store"
"git.t-juice.club/torjus/apiary/web" "git.t-juice.club/torjus/apiary/web"
"github.com/coreos/go-systemd/daemon" "github.com/coreos/go-systemd/daemon"
sshlib "github.com/gliderlabs/ssh" sshlib "github.com/gliderlabs/ssh"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v3"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"golang.org/x/crypto/acme/autocert" "golang.org/x/crypto/acme/autocert"
) )
func main() { func main() {
app := &cli.App{ app := &cli.Command{
Name: "apiary", Name: "apiary",
Version: apiary.FullVersion(), Version: apiary.FullVersion(),
Authors: []*cli.Author{
{
Name: "Torjus Håkestad",
Email: "torjus@usit.uio.no",
},
},
Commands: []*cli.Command{ Commands: []*cli.Command{
{ {
Name: "serve", Name: "serve",
@ -43,13 +34,13 @@ func main() {
}, },
} }
if err := app.Run(os.Args); err != nil { if err := app.Run(context.Background(), os.Args); err != nil {
fmt.Printf("Error: %s\n", err) fmt.Printf("Error: %s\n", err)
os.Exit(1) os.Exit(1)
} }
} }
func ActionServe(c *cli.Context) error { func ActionServe(ctx context.Context, cmd *cli.Command) error {
cfg, err := getConfig() cfg, err := getConfig()
if err != nil { if err != nil {
return err return err
@ -57,17 +48,17 @@ func ActionServe(c *cli.Context) error {
// Setup logging // Setup logging
loggers := setupLoggers(cfg) loggers := setupLoggers(cfg)
loggers.rootLogger.Infow("Starting apiary", "version", apiary.FullVersion()) loggers.rootLogger.Info("Starting apiary.", "version", apiary.FullVersion())
// Setup store // Setup store
var s store.LoginAttemptStore var s store.LoginAttemptStore
switch cfg.Store.Type { switch cfg.Store.Type {
case "MEMORY", "memory": case "MEMORY", "memory":
loggers.rootLogger.Infow("Initialized store", "store_type", "memory") loggers.rootLogger.Info("Initialized store.", "store_type", "memory")
s = &store.MemoryStore{} s = &store.MemoryStore{}
case "POSTGRES", "postgres": case "POSTGRES", "postgres":
pgStartTime := time.Now() pgStartTime := time.Now()
loggers.rootLogger.Debugw("Initializing store", "store_type", "postgres") loggers.rootLogger.Debug("Initializing store.", "store_type", "postgres")
pgStore, err := store.NewPostgresStore(cfg.Store.Postgres.DSN) pgStore, err := store.NewPostgresStore(cfg.Store.Postgres.DSN)
if err != nil { if err != nil {
return err return err
@ -75,13 +66,13 @@ func ActionServe(c *cli.Context) error {
if err := pgStore.InitDB(); err != nil { if err := pgStore.InitDB(); err != nil {
return err return err
} }
loggers.rootLogger.Infow("Initialized store", "store_type", "postgres", "init_time", time.Since(pgStartTime)) loggers.rootLogger.Info("Initialized store.", "store_type", "postgres", "init_time", time.Since(pgStartTime))
if cfg.Store.EnableCache { if cfg.Store.EnableCache {
loggers.rootLogger.Debugw("Initializing store", "store_type", "cache-postgres") loggers.rootLogger.Debug("Initializing store.", "store_type", "cache-postgres")
startTime := time.Now() startTime := time.Now()
cachingStore := store.NewCachingStore(pgStore) cachingStore := store.NewCachingStore(pgStore)
s = cachingStore s = cachingStore
loggers.rootLogger.Infow("Initialized store", "store_type", "cache-postgres", "init_time", time.Since(startTime)) loggers.rootLogger.Info("Initialized store.", "store_type", "cache-postgres", "init_time", time.Since(startTime))
} else { } else {
s = pgStore s = pgStore
} }
@ -93,7 +84,7 @@ func ActionServe(c *cli.Context) error {
interruptChan := make(chan os.Signal, 1) interruptChan := make(chan os.Signal, 1)
signal.Notify(interruptChan, os.Interrupt) signal.Notify(interruptChan, os.Interrupt)
rootCtx, rootCancel := context.WithCancel(c.Context) rootCtx, rootCancel := context.WithCancel(ctx)
defer rootCancel() defer rootCancel()
serversCtx, serversCancel := context.WithCancel(rootCtx) serversCtx, serversCancel := context.WithCancel(rootCtx)
defer serversCancel() defer serversCancel()
@ -126,78 +117,27 @@ func ActionServe(c *cli.Context) error {
web.TLSConfig = tlsConfig web.TLSConfig = tlsConfig
} }
// Setup portlistener, if configured
if cfg.Ports.Enable {
portsCtx, cancel := context.WithCancel(rootCtx)
defer cancel()
// TODO: Add more stores
store := &ports.MemoryStore{}
portsServer := ports.New(store)
portsServer.Logger = loggers.portsLogger
for _, port := range cfg.Ports.TCPPorts {
portsServer.AddTCPPort(port)
}
go func() {
loggers.rootLogger.Info("Starting ports server")
portsServer.Start(portsCtx)
}()
}
// Setup smtp honeypot if enabled
if cfg.SMTP.Enable {
honeypot, err := smtp.NewSMTPHoneypot()
if err != nil {
loggers.rootLogger.Warnw("Error seting up SMTP honeypot", "error", err)
}
honeypot.Addr = cfg.SMTP.Addr
honeypot.Logger = loggers.smtpLogger
if cfg.SMTP.EnableMetrics {
honeypot.Store = smtp.NewMetricsStore(&smtp.DiscardStore{})
} else {
honeypot.Store = &smtp.DiscardStore{}
}
// Start smtp honeypot
go func() {
loggers.rootLogger.Info("Starting SMTP server")
if err := honeypot.ListenAndServe(); err != nil {
loggers.rootLogger.Warnw("SMTP server returned error", "error", err)
}
}()
// Wait for smtp shutdown
go func() {
<-serversCtx.Done()
loggers.rootLogger.Info("SMTP server shutdown started")
if err := honeypot.Shutdown(); err != nil {
loggers.rootLogger.Errorw("Error shutting down SMTP server", "error", err)
}
loggers.rootLogger.Info("SMTP server shutdown complete")
}()
}
// Handle interrupt // Handle interrupt
go func() { go func() {
<-interruptChan <-interruptChan
loggers.rootLogger.Info("Interrupt received, shutting down") loggers.rootLogger.Info("Interrupt received, shutting down.")
serversCancel() serversCancel()
}() }()
// Start ssh server // Start ssh server
go func() { go func() {
loggers.rootLogger.Info("Starting SSH server") loggers.rootLogger.Info("Starting SSH server.", "addr", cfg.Honeypot.ListenAddr)
if err := hs.ListenAndServe(); err != nil && err != sshlib.ErrServerClosed { if err := hs.ListenAndServe(); err != nil && err != sshlib.ErrServerClosed {
loggers.rootLogger.Warnw("SSH server returned error", "error", err) loggers.rootLogger.Warn("SSH server returned error.", "error", err)
} }
loggers.rootLogger.Info("SSH server stopped.")
}() }()
// Start web server // Start web server
go func() { go func() {
loggers.rootLogger.Info("Starting web server") loggers.rootLogger.Info("Starting web server.", "addr", cfg.Frontend.ListenAddr)
if err := web.StartServe(); err != nil && err != http.ErrServerClosed { if err := web.StartServe(); err != nil && err != http.ErrServerClosed {
loggers.rootLogger.Warnw("Web server returned error", "error", err) loggers.rootLogger.Warn("Web server returned error.", "error", err)
} }
}() }()
@ -217,7 +157,7 @@ func ActionServe(c *cli.Context) error {
return return
} }
if err != nil { if err != nil {
loggers.rootLogger.Warnw("Unable to connect to NOTIFY_SOCKET.", "error", err) loggers.rootLogger.Warn("Unable to connect to NOTIFY_SOCKET.", "error", err)
return return
} }
loggers.rootLogger.Debug("Sent READY=1 to NOTIFY_SOCKET.") loggers.rootLogger.Debug("Sent READY=1 to NOTIFY_SOCKET.")
@ -225,11 +165,11 @@ func ActionServe(c *cli.Context) error {
// Setup timer // Setup timer
timeout, err := daemon.SdWatchdogEnabled(false) timeout, err := daemon.SdWatchdogEnabled(false)
if err != nil { if err != nil {
loggers.rootLogger.Warnw("Unable to get watchdog timeout.", "error", err) loggers.rootLogger.Warn("Unable to get watchdog timeout.", "error", err)
return return
} }
if timeout == 0 { if timeout == 0 {
loggers.rootLogger.Infow("Systemd watchdog not enabled.") loggers.rootLogger.Info("Systemd watchdog not enabled.")
return return
} }
@ -241,13 +181,15 @@ func ActionServe(c *cli.Context) error {
select { select {
case <-ticker.C: case <-ticker.C:
if healthy == nil { if healthy == nil {
daemon.SdNotify(false, daemon.SdNotifyWatchdog) if _, err := daemon.SdNotify(false, daemon.SdNotifyWatchdog); err != nil {
loggers.rootLogger.Warn("Error notifying watchdog.", "err", err)
}
continue continue
} }
// TODO: If unhealthy, should we retry healthcheck immediately, otherwise service will most likely get killed by watchdog. // TODO: If unhealthy, should we retry healthcheck immediately, otherwise service will most likely get killed by watchdog.
loggers.rootLogger.Errorw("Store reported not healthy, might get killed by watchdog.", "err", healthy) loggers.rootLogger.Error("Store reported not healthy, might get killed by watchdog.", "err", healthy)
case <-notifyCtx.Done(): case <-notifyCtx.Done():
loggers.rootLogger.Debugw("Notify context cancelled.") loggers.rootLogger.Debug("Notify context cancelled.")
return return
} }
} }
@ -259,80 +201,61 @@ func ActionServe(c *cli.Context) error {
// Stop SSH server // Stop SSH server
sshShutdownCtx, sshShutdownCancel := context.WithTimeout(context.Background(), 10*time.Second) sshShutdownCtx, sshShutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer sshShutdownCancel() defer sshShutdownCancel()
loggers.rootLogger.Info("SSH server shutdown started") loggers.rootLogger.Info("SSH server shutdown started.")
if err := hs.Shutdown(sshShutdownCtx); err != nil { if err := hs.Shutdown(sshShutdownCtx); err != nil {
loggers.rootLogger.Infow("Error shutting down SSH server", "error", err) loggers.rootLogger.Info("Error shutting down SSH server.", "error", err)
} }
loggers.rootLogger.Info("SSH server shutdown complete") loggers.rootLogger.Info("SSH server shutdown complete.")
// Stop Web server // Stop Web server
webShutdownCtx, webShutdownCancel := context.WithTimeout(context.Background(), 10*time.Second) webShutdownCtx, webShutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer webShutdownCancel() defer webShutdownCancel()
loggers.rootLogger.Info("Web server shutdown started") loggers.rootLogger.Info("Web server shutdown started.")
if err := web.Shutdown(webShutdownCtx); err != nil { if err := web.Shutdown(webShutdownCtx); err != nil {
loggers.rootLogger.Infow("Error shutting down web server", "error", err) loggers.rootLogger.Info("Error shutting down web server.", "error", err)
} }
loggers.rootLogger.Info("Web server shutdown complete") loggers.rootLogger.Info("Web server shutdown complete.")
rootCancel() rootCancel()
}() }()
<-rootCtx.Done() <-rootCtx.Done()
return nil return nil
} }
type loggerCollection struct { type loggerCollection struct {
rootLogger *zap.SugaredLogger rootLogger *slog.Logger
honeypotLogger *zap.SugaredLogger honeypotLogger *slog.Logger
webAccessLogger *zap.SugaredLogger webAccessLogger *slog.Logger
webServerLogger *zap.SugaredLogger webServerLogger *slog.Logger
portsLogger *zap.SugaredLogger
smtpLogger *zap.SugaredLogger
} }
func setupLoggers(cfg config.Config) *loggerCollection { func setupLoggers(cfg config.Config) *loggerCollection {
logEncoderCfg := zap.NewProductionEncoderConfig() opts := &slog.HandlerOptions{}
logEncoderCfg.EncodeCaller = func(caller zapcore.EntryCaller, enc zapcore.PrimitiveArrayEncoder) {}
var level zap.AtomicLevel
switch strings.ToUpper(cfg.Honeypot.LogLevel) { switch strings.ToUpper(cfg.Honeypot.LogLevel) {
case "INFO": case "INFO":
level = zap.NewAtomicLevelAt(zap.InfoLevel) opts.Level = slog.LevelInfo
case "DEBUG": case "DEBUG":
level = zap.NewAtomicLevelAt(zap.DebugLevel) opts.Level = slog.LevelDebug
opts.AddSource = true
case "WARN", "WARNING": case "WARN", "WARNING":
level = zap.NewAtomicLevelAt(zap.WarnLevel) opts.Level = slog.LevelWarn
case "ERR", "ERROR": case "ERR", "ERROR":
level = zap.NewAtomicLevelAt(zap.WarnLevel) opts.Level = slog.LevelError
default: default:
level = zap.NewAtomicLevelAt(zap.InfoLevel) opts.Level = slog.LevelInfo
}
logEncoderCfg.EncodeLevel = zapcore.CapitalColorLevelEncoder
logEncoderCfg.EncodeTime = zapcore.ISO8601TimeEncoder
logEncoderCfg.EncodeDuration = zapcore.StringDurationEncoder
rootLoggerCfg := &zap.Config{
Level: level,
OutputPaths: []string{"stdout"},
ErrorOutputPaths: []string{"stderr"},
Encoding: "console",
EncoderConfig: logEncoderCfg,
}
rootLogger, err := rootLoggerCfg.Build()
if err != nil {
panic(err)
} }
handler := slog.NewTextHandler(os.Stdout, opts)
rootLogger := slog.New(handler)
return &loggerCollection{ return &loggerCollection{
rootLogger: rootLogger.Named("APP").Sugar(), rootLogger: rootLogger.With("module", "application"),
honeypotLogger: rootLogger.Named("HON").Sugar(), honeypotLogger: rootLogger.With("module", "honeypot"),
webAccessLogger: rootLogger.Named("ACC").Sugar(), webAccessLogger: rootLogger.With("module", "web-access-log"),
webServerLogger: rootLogger.Named("WEB").Sugar(), webServerLogger: rootLogger.With("module", "web-server"),
portsLogger: rootLogger.Named("PRT").Sugar(),
smtpLogger: rootLogger.Named("SMT").Sugar(),
} }
} }
func getConfig() (config.Config, error) { func getConfig() (config.Config, error) {

View File

@ -6,15 +6,13 @@ import (
"os" "os"
"strings" "strings"
"github.com/pelletier/go-toml" "github.com/pelletier/go-toml/v2"
) )
type Config struct { type Config struct {
Store StoreConfig `toml:"Store"` Store StoreConfig `toml:"Store"`
Honeypot HoneypotConfig `toml:"Honeypot"` Honeypot HoneypotConfig `toml:"Honeypot"`
Frontend FrontendConfig `toml:"Frontend"` Frontend FrontendConfig `toml:"Frontend"`
Ports PortsConfig `toml:"Ports"`
SMTP SMTPConfig `toml:"SMTP"`
} }
type StoreConfig struct { type StoreConfig struct {
Type string `toml:"Type"` Type string `toml:"Type"`
@ -34,10 +32,12 @@ type HoneypotConfig struct {
} }
type FrontendConfig struct { type FrontendConfig struct {
ListenAddr string `toml:"ListenAddr"` ListenAddr string `toml:"ListenAddr"`
LogLevel string `toml:"LogLevel"` LogLevel string `toml:"LogLevel"`
AccessLogEnable bool `toml:"AccessLogEnable"` MetricsEnable bool `toml:"MetricsEnable"`
Autocert FrontendAutocertConfig `toml:"Autocert"` AccessLogEnable bool `toml:"AccessLogEnable"`
AccessLogIgnoreMetrics bool `toml:"AccessLogIgnoreMetrics"`
Autocert FrontendAutocertConfig `toml:"Autocert"`
} }
type FrontendAutocertConfig struct { type FrontendAutocertConfig struct {
@ -48,19 +48,6 @@ type FrontendAutocertConfig struct {
RedirectHTTP bool `toml:"RedirectHTTP"` RedirectHTTP bool `toml:"RedirectHTTP"`
} }
type PortsConfig struct {
Enable bool `toml:"Enable"`
Addr string `toml:"Addr"`
TCPPorts []string `toml:"TCPPorts"`
UDPPorts []string `toml:"UDPPorts"`
}
type SMTPConfig struct {
Enable bool `toml:"Enable"`
Addr string `toml:"Addr"`
EnableMetrics bool `toml:"EnableMetrics"`
}
func FromReader(r io.Reader) (Config, error) { func FromReader(r io.Reader) (Config, error) {
var c Config var c Config
@ -70,7 +57,6 @@ func FromReader(r io.Reader) (Config, error) {
c.Frontend.ListenAddr = ":8080" c.Frontend.ListenAddr = ":8080"
c.Frontend.LogLevel = "INFO" c.Frontend.LogLevel = "INFO"
c.Frontend.AccessLogEnable = true c.Frontend.AccessLogEnable = true
c.SMTP.Addr = ":25"
// Read from config // Read from config
decoder := toml.NewDecoder(r) decoder := toml.NewDecoder(r)
@ -78,7 +64,7 @@ func FromReader(r io.Reader) (Config, error) {
return c, fmt.Errorf("unable to parse config: %w", err) return c, fmt.Errorf("unable to parse config: %w", err)
} }
//c.readEnv() // c.readEnv()
return c, nil return c, nil
} }

27
flake.lock generated Normal file
View File

@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1742288794,
"narHash": "sha256-Txwa5uO+qpQXrNG4eumPSD+hHzzYi/CdaM80M9XRLCo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b6eaf97c6960d97350c584de1b6dcff03c9daf42",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

123
flake.nix Normal file
View File

@ -0,0 +1,123 @@
{
description = "SSH honeypot with frontend";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs =
{ self, nixpkgs }:
let
allSystems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
forAllSystems =
f:
nixpkgs.lib.genAttrs allSystems (
system:
f {
pkgs = import nixpkgs { inherit system; };
}
);
in
{
overlays.default = final: prev: {
apiary = self.packages.${prev.system}.default;
};
packages = forAllSystems (
{ pkgs }:
{
default = self.packages.${pkgs.system}.apiary;
apiary =
let
src = pkgs.lib.sourceFilesBySuffices ./. [
"go.mod"
"go.sum"
".go"
];
version = pkgs.lib.strings.removePrefix "v" (
builtins.elemAt (pkgs.lib.strings.split "\"" (
pkgs.lib.lists.findFirst (x: pkgs.lib.strings.hasInfix "Version" x) null (
pkgs.lib.strings.splitString "\n" (builtins.readFile ./version.go)
)
)) 2
);
rev = self.rev or "";
geoDb = pkgs.fetchFromGitHub {
owner = "geoacumen";
repo = "geoacumen-country";
rev = "5f770af620465f40427c3f421446a7b7845e2699";
sha256 = "sha256-t3ELkhAbJC40z6r6wJeQI4Kutfw26fRg6n7a6FmhvkA=";
};
frontend = pkgs.buildNpmPackage {
inherit version;
name = "apiary-frontend";
src = ./frontend;
npmDepsHash = "sha256-7gjPEiQAMOpteLwHT4AIY9QLpDozpXb7xy0bDiOpDf4=";
installPhase = ''
mkdir -p $out
cp -r dist/* $out
'';
};
in
pkgs.buildGoModule {
inherit version;
pname = "apiary";
src = src;
prePatch = ''
cp ${geoDb}/Geoacumen-Country.mmdb honeypot/ssh
mkdir -p web/frontend/dist
cp -r ${frontend}/* web/frontend/dist
'';
vendorHash = "sha256-t/DNKefG+UxZEfNYNUmOPSw6aqgNuz6v7qxow5JRXsE=";
ldflags = [ "-X git.t-juice.club/torjus/apiary.Build=${rev}" ];
tags = [
"embed"
];
};
tarball =
let
version = self.packages.${pkgs.system}.apiary.version;
in
pkgs.stdenv.mkDerivation {
name = "apiary-tarballs-${version}";
phases = [ "installPhase" ];
installPhase = ''
mkdir -p $out
mkdir apiary
cp ${self.packages.${pkgs.system}.apiary}/bin/apiary apiary/apiary-${pkgs.system}-${version}
tar cvzf $out/apiary-${pkgs.system}-${version}.tar.gz apiary
pushd apiary
sha256sum apiary-${pkgs.system}-${version} > apiary-${pkgs.system}-${version}.sha256sum
popd
cp apiary/apiary-${pkgs.system}-${version}.sha256sum $out
'';
};
}
);
devShells = forAllSystems (
{ pkgs }:
{
default = pkgs.mkShell {
packages = with pkgs; [
go
golangci-lint
];
};
frontend = pkgs.mkShell {
packages = with pkgs; [
nodejs
yarn
];
};
}
);
};
}

7
frontend/jest.config.js Normal file
View File

@ -0,0 +1,7 @@
/** @type {import('ts-jest').JestConfigWithTsJest} **/
module.exports = {
testEnvironment: "jsdom",
transform: {
"^.+\.tsx?$": ["ts-jest",{}],
},
};

9405
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

37
frontend/package.json Normal file
View File

@ -0,0 +1,37 @@
{
"name": "apiary-frontend",
"scripts": {
"start": "parcel src/index.html",
"build": "parcel build src/index.html",
"test": "jest"
},
"devDependencies": {
"@ngneat/falso": "^7.3.0",
"@parcel/transformer-css": "^2.13.3",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
"@types/humanize-duration": "^3.27.4",
"@types/jest": "^29.5.14",
"@types/node": "^22.13.9",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"parcel": "^2.13.3",
"process": "^0.11.10",
"svgo": "^3.3.2",
"ts-jest": "^29.2.6",
"ts-loader": "^9.5.2",
"typescript": "^5.8.2"
},
"dependencies": {
"chart.js": "^4.4.8",
"humanize-duration": "^3.32.1",
"react": "^19.0.0",
"react-chartjs-2": "^5.3.0",
"react-dom": "^19.0.0",
"react-router": "^7.2.0"
}
}

View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 511.573 511.573" xml:space="preserve">
<g transform="translate(1 1)">
<polygon style="fill:#FFE100;" points="254.787,7.107 84.12,143.64 425.453,143.64 "/>
<polygon style="fill:#FFFFFF;" points="84.12,143.64 254.787,7.107 41.453,143.64 "/>
<polygon style="fill:#FFA800;" points="254.787,7.107 425.453,143.64 468.12,143.64 "/>
<g>
<polygon style="fill:#FFE100;" points="41.453,502.04 92.653,502.04 92.653,450.84 41.453,450.84 "/>
<polygon style="fill:#FFE100;" points="416.92,502.04 468.12,502.04 468.12,450.84 416.92,450.84 "/>
<polygon style="fill:#FFE100;" points="41.453,450.84 468.12,450.84 468.12,143.64 41.453,143.64 "/>
</g>
<polygon style="fill:#FFA800;" points="442.52,126.573 442.52,502.04 468.12,502.04 468.12,146.2 "/>
<polygon style="fill:#FFFFFF;" points="41.453,143.64 41.453,502.04 67.053,502.04 67.053,126.573 "/>
<path style="fill:#63D3FD;" d="M314.52,450.84H195.053V341.613c0-15.36,12.8-27.307,27.307-27.307h64
c15.36,0,27.307,12.8,27.307,27.307V450.84H314.52z"/>
<path style="fill:#3DB9F9;" d="M287.213,314.307h-25.6c15.36,0,27.307,12.8,27.307,27.307V450.84h25.6V341.613
C314.52,327.107,302.573,314.307,287.213,314.307"/>
<path d="M468.12,459.373H41.453c-5.12,0-8.533-3.413-8.533-8.533v-307.2c0-2.56,1.707-5.973,4.267-6.827L250.52,0.28
c2.56-1.707,5.973-1.707,9.387,0L473.24,136.813c2.56,1.707,4.267,4.267,4.267,6.827v307.2
C476.653,455.96,473.24,459.373,468.12,459.373z M49.987,442.307h409.6v-294.4l-204.8-130.56l-204.8,130.56V442.307z"/>
<path d="M92.653,510.573h-51.2c-5.12,0-8.533-3.413-8.533-8.533v-51.2c0-5.12,3.413-8.533,8.533-8.533h51.2
c5.12,0,8.533,3.413,8.533,8.533v51.2C101.187,507.16,97.773,510.573,92.653,510.573z M49.987,493.507H84.12v-34.133H49.987
V493.507z"/>
<path d="M468.12,510.573h-51.2c-5.12,0-8.533-3.413-8.533-8.533v-51.2c0-5.12,3.413-8.533,8.533-8.533h51.2
c5.12,0,8.533,3.413,8.533,8.533v51.2C476.653,507.16,473.24,510.573,468.12,510.573z M425.453,493.507h34.133v-34.133h-34.133
V493.507z"/>
<path d="M7.32,177.773c-2.56,0-5.12-0.853-6.827-3.413c-2.56-3.413-1.707-9.387,1.707-11.947l34.133-25.6
c3.413-2.56,9.387-1.707,11.947,1.707c2.56,3.413,1.707,9.387-1.707,11.947l-34.133,25.6C10.733,176.92,9.027,177.773,7.32,177.773
z"/>
<path d="M502.253,177.773c-1.707,0-3.413-0.853-5.12-1.707L463,150.467c-3.413-2.56-4.267-8.533-1.707-11.947
c2.56-3.413,8.533-4.267,11.947-1.707l34.133,25.6c3.413,2.56,4.267,8.533,1.707,11.947
C507.373,176.92,504.813,177.773,502.253,177.773z"/>
<path d="M468.12,254.573H41.453c-5.12,0-8.533-3.413-8.533-8.533v-102.4c0-5.12,3.413-8.533,8.533-8.533H468.12
c5.12,0,8.533,3.413,8.533,8.533v102.4C476.653,251.16,473.24,254.573,468.12,254.573z M49.987,237.507h409.6v-85.333h-409.6
V237.507z"/>
<path d="M314.52,459.373H195.053c-5.12,0-8.533-3.413-8.533-8.533V341.613c0-19.627,16.213-35.84,35.84-35.84h64
c19.627,0,35.84,16.213,35.84,35.84V450.84C323.053,455.96,319.64,459.373,314.52,459.373z M203.587,442.307h102.4V341.613
c0-10.24-8.533-18.773-18.773-18.773h-64c-11.093,0-19.627,8.533-19.627,18.773V442.307z"/>
<path d="M195.053,459.373h-153.6c-5.12,0-8.533-3.413-8.533-8.533v-102.4c0-5.12,3.413-8.533,8.533-8.533h153.6
c5.12,0,8.533,3.413,8.533,8.533v102.4C203.587,455.96,200.173,459.373,195.053,459.373z M49.987,442.307H186.52v-85.333H49.987
V442.307z"/>
<path d="M468.12,459.373h-153.6c-5.12,0-8.533-3.413-8.533-8.533v-102.4c0-5.12,3.413-8.533,8.533-8.533h153.6
c5.12,0,8.533,3.413,8.533,8.533v102.4C476.653,455.96,473.24,459.373,468.12,459.373z M323.053,442.307h136.533v-85.333H323.053
V442.307z"/>
<path d="M468.12,356.973h-153.6c-5.12,0-8.533-3.413-8.533-8.533v-6.827c0-10.24-8.533-18.773-18.773-18.773h-64
c-11.093,0-19.627,8.533-19.627,18.773v6.827c0,5.12-3.413,8.533-8.533,8.533h-153.6c-5.12,0-8.533-3.413-8.533-8.533v-102.4
c0-5.12,3.413-8.533,8.533-8.533H468.12c5.12,0,8.533,3.413,8.533,8.533v102.4C476.653,353.56,473.24,356.973,468.12,356.973z
M323.053,339.907h136.533v-85.333h-409.6v85.333H186.52c0.853-18.773,17.067-34.133,35.84-34.133h64
C305.987,305.773,322.2,321.133,323.053,339.907z"/>
<path d="M468.12,288.707h-59.733c-5.12,0-8.533-3.413-8.533-8.533c0-5.12,3.413-8.533,8.533-8.533h59.733
c5.12,0,8.533,3.413,8.533,8.533S473.24,288.707,468.12,288.707z"/>
<path d="M468.12,322.84h-34.133c-5.12,0-8.533-3.413-8.533-8.533c0-5.12,3.413-8.533,8.533-8.533h34.133
c5.12,0,8.533,3.413,8.533,8.533C476.653,319.427,473.24,322.84,468.12,322.84z"/>
<path d="M101.187,288.707H41.453c-5.12,0-8.533-3.413-8.533-8.533c0-5.12,3.413-8.533,8.533-8.533h59.733
c5.12,0,8.533,3.413,8.533,8.533S106.307,288.707,101.187,288.707z"/>
<path d="M75.587,322.84H41.453c-5.12,0-8.533-3.413-8.533-8.533c0-5.12,3.413-8.533,8.533-8.533h34.133
c5.12,0,8.533,3.413,8.533,8.533C84.12,319.427,80.707,322.84,75.587,322.84z"/>
<path d="M468.12,186.307h-59.733c-5.12,0-8.533-3.413-8.533-8.533c0-5.12,3.413-8.533,8.533-8.533h59.733
c5.12,0,8.533,3.413,8.533,8.533C476.653,182.893,473.24,186.307,468.12,186.307z"/>
<path d="M468.12,220.44h-34.133c-5.12,0-8.533-3.413-8.533-8.533c0-5.12,3.413-8.533,8.533-8.533h34.133
c5.12,0,8.533,3.413,8.533,8.533C476.653,217.027,473.24,220.44,468.12,220.44z"/>
<path d="M101.187,186.307H41.453c-5.12,0-8.533-3.413-8.533-8.533c0-5.12,3.413-8.533,8.533-8.533h59.733
c5.12,0,8.533,3.413,8.533,8.533C109.72,182.893,106.307,186.307,101.187,186.307z"/>
<path d="M75.587,220.44H41.453c-5.12,0-8.533-3.413-8.533-8.533c0-5.12,3.413-8.533,8.533-8.533h34.133
c5.12,0,8.533,3.413,8.533,8.533C84.12,217.027,80.707,220.44,75.587,220.44z"/>
<path d="M468.12,391.107h-59.733c-5.12,0-8.533-3.413-8.533-8.533c0-5.12,3.413-8.533,8.533-8.533h59.733
c5.12,0,8.533,3.413,8.533,8.533C476.653,387.693,473.24,391.107,468.12,391.107z"/>
<path d="M468.12,425.24h-34.133c-5.12,0-8.533-3.413-8.533-8.533s3.413-8.533,8.533-8.533h34.133c5.12,0,8.533,3.413,8.533,8.533
S473.24,425.24,468.12,425.24z"/>
<path d="M101.187,391.107H41.453c-5.12,0-8.533-3.413-8.533-8.533c0-5.12,3.413-8.533,8.533-8.533h59.733
c5.12,0,8.533,3.413,8.533,8.533C109.72,387.693,106.307,391.107,101.187,391.107z"/>
<path d="M75.587,425.24H41.453c-5.12,0-8.533-3.413-8.533-8.533s3.413-8.533,8.533-8.533h34.133c5.12,0,8.533,3.413,8.533,8.533
S80.707,425.24,75.587,425.24z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -0,0 +1,2 @@
@import url("https://fonts.googleapis.com/css2?family=Prompt:wght@300;600&display=swap");
@import url('https://fonts.googleapis.com/css2?family=Secular+One&display=swap');

View File

@ -0,0 +1,211 @@
:root {
--main-color-bg: white;
--main-color: black;
/* Menu */
--menu-color-bg: #212529;
--menu-color-text: hsla(0,0%,100%,.5);
--menu-color-text-hover: white;
--menu-color-title-text: white;
/* Table */
--table-color-header-bg: var(--menu-color-bg);
--table-color-header-text: #dddddd;
--table-row-odd: #dddddd;
--table-row-even: #f3f3f3;
}
.dark_mode {
--main-color-bg: #15141a;
--main-color: white;
/* Menu */
--menu-color-bg: #35393d;
--menu-color-text: hsla(0,0%,100%,.5);
--menu-color-text-hover: white;
--menu-color-title-text: white;
/* Table */
--table-color-header-bg: var(--menu-color-bg);
--table-color-header-text: var(--menu-color-text);
--table-row-odd: #302f36;
--table-row-even: #232229;
}
#root {
padding: 0;
margin: 0;
height: 100%;
width: 100%;
}
body {
background-color: var(--main-color-bg);
color: var(--main-color);
margin: 0;
margin-top: 100px;
}
*,
*::before,
*::after {
padding: 0;
margin: 0;
}
a {
text-decoration: none;
}
canvas {
width: 100%;
display: block;
padding-left: 0;
padding-right: 0;
margin-left: auto;
margin-right: auto;
}
form {
width: 100%;
}
input[type=text] {
width: 100%;
height: 30px;
box-sizing: border-box;
border: 2px solid var(--menu-color-bg);
border-radius: 5px;
padding-left: 5px;
}
.navbar {
background-color: var(--menu-color-bg);
display: flex;
font-size: 1.2rem;
font-family: "Prompt", sans-serif;
height: 40px;
position: fixed;
top: 0;
width: 100%;
}
.navbar ul {
display: flex;
align-items: center;
}
.root .navbar {
justify-content: space-between;
}
.navbar ul li {
list-style-type: none;
color: var(--menu-color-text);
padding-left: 10px;
padding-right: 10px;
line-height: 40px;
}
.navbar h2 {
line-height: 40px;
align-items: center;
padding-right: 10px;
}
.navbar a {
height: 1;
}
.navbar a:hover {
color: var(--menu-color-text-hover);
}
.submenu nav {
display: flex;
justify-content: space-between;
line-height: 1;
}
.totals {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: 1fr;
grid-column-gap: 0px;
grid-row-gap: 0px;
margin-top: 5px;
margin-bottom: 5px;
font-family: "Secular One", sans-serif;
font-size: 20px;
font-weight: 200;
}
.totals_key {
grid-area: 1 / 1 / 2 / 2;
text-align: right;
}
.totals_value {
grid-area: 1 / 2 / 2 / 3;
padding-left: 10px;
text-align: left;
}
#menu_title {
color: var(--menu-color-text-hover);
font-family: "Secular One", sans-serif;
font-size: 30px;
font-weight: 300;
}
#menu_logo {
padding: 5px;
content: url("../assets/apiary.svg");
width: 30px;
height: 30px;
}
.menu_link {
color: var(--menu-color-text);
}
.menu_link:hover {
color: var(--menu-color-text-hover);
}
.menu_link_active {
color: var(--menu-color-text-hover);
}
.stats_pie {
max-height: 70vh;
text-align: left;
color: white;
}
.live_table {
border-collapse: collapse;
margin: 25px 0;
font-size: 0.9em;
font-family: sans-serif;
min-width: 400px;
width: 100%;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
}
.live_table thead tr {
background-color: var(--table-color-header-bg);
color: var(--table-color-header-text);
text-align: left;
}
.live_table th,
.live_table td {
padding: 12px 15px;
text-align: left;
}
.live_table tbody tr {
border-bottom: 1px solid var(--table-row-odd);
}
.live_table tbody tr:nth-of-type(even) {
background-color: var(--table-row-even);
}
.live_table tbody tr:last-of-type {
border-bottom: 2px solid var(--table-color-bg);
}
.live_table tbody tr.active-row {
font-weight: bold;
color: var(--table-color-bg);
}
.content {
margin-left: 10vw;
width: 50vw;
text-align: center;
}

12
frontend/src/index.html Normal file
View File

@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title></title>
<link rel="stylesheet" href="./css/fonts.css" />
<script type="module" src="./js/app.tsx"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>

154
frontend/src/js/api.ts Normal file
View File

@ -0,0 +1,154 @@
import { randUserName, randRecentDate, randIp, randPassword, randUuid, randNumber } from '@ngneat/falso';
import { r } from 'react-router/dist/development/fog-of-war-Cm1iXIp7';
export interface LoginAttempt {
readonly date: string;
readonly remoteIP: string;
readonly username: string;
readonly password: string;
readonly sshClientVersion: string;
readonly connectionUUID: string;
readonly country: string;
}
export interface TotalStats {
readonly password: number;
readonly username: number;
readonly ip: number;
readonly country: number;
readonly attempts: number;
}
export interface StatsResult {
name: string;
count: number;
}
export type StatsType = 'password' | 'username' | 'ip' | 'country' | 'attempts';
export interface ApiaryAPI {
live(fn: (a: LoginAttempt) => void): void;
stats(statsType: StatsType, limit: number): Promise<StatsResult[]>;
query(queryType: string, query: string): Promise<LoginAttempt[]>;
totals(): Promise<TotalStats>;
}
function fakeLoginAttempt(): LoginAttempt {
return {
date: randRecentDate({ days: 2 }).toISOString(),
remoteIP: randIp().toString(),
username: randUserName().toString(),
password: randPassword().toString(),
sshClientVersion: '1.0',
connectionUUID: randUuid().toString(),
country: 'NO'
}
}
export class DummyApiaryAPIClient implements ApiaryAPI {
live(fn: (a: LoginAttempt) => void): () => void {
const interval = setInterval(() => {
let a = fakeLoginAttempt();
fn(a);
}, 1000);
return () => { clearInterval(interval) }
}
async stats(_type: StatsType, limit: number): Promise<StatsResult[]> {
const stats = Array.from({ length: limit }, () => {
switch (_type) {
case 'password':
return { name: randPassword().toString(), count: randNumber().valueOf() }
case 'username':
return { name: randUserName().toString(), count: randNumber().valueOf() }
case 'ip':
return { name: randIp().toString(), count: randNumber().valueOf() }
case 'country':
return { name: 'NO', count: randNumber().valueOf() }
}
return { name: randUserName().toString(), count: randNumber().valueOf() }
});
const sorted = stats.sort((a, b) => b.count - a.count)
return Promise.resolve(sorted);
}
async query(_type: string, _query: string): Promise<LoginAttempt[]> {
const attempts = Array.from({ length: 10 }, () => {
return fakeLoginAttempt();
})
return Promise.resolve(attempts);
}
async totals(): Promise<TotalStats> {
return Promise.resolve({ password: 1, username: 1, ip: 1, country: 1, attempts: 1 })
}
}
export class ApiaryAPIClient implements ApiaryAPI {
live(fn: (a: LoginAttempt) => void): () => void {
const es = new EventSource('/api/stream');
const updateFn = (ev: MessageEvent<string>) => {
const attempt: LoginAttempt = JSON.parse(ev.data);
fn(attempt);
};
es.addEventListener('message', updateFn)
return () => {
es.removeEventListener('message', updateFn);
es.close();
}
}
async stats(statsType: StatsType, limit: number): Promise<StatsResult[]> {
const resp = await fetch(`/api/stats?type=${statsType}&limit=${limit}`)
if (!resp.ok) {
throw new Error('Failed to fetch query')
}
const data: StatsResult[] | null = await resp.json()
if (!data) {
return []
}
return data.sort((a, b) => b.count - a.count)
}
async query(queryType: string, query: string): Promise<LoginAttempt[]> {
const resp = await fetch(`/api/query?type=${queryType}&query=${query}`)
if (!resp.ok) {
throw new Error('Failed to fetch query')
}
const data: LoginAttempt[] = await resp.json()
return data
}
async totals(): Promise<TotalStats> {
let password: number = -1
let username: number = -1
let ip: number = -1
let attempts: number = -1
let country: number = -1
const resp = await fetch('/api/stats?type=total')
const data: Array<StatsResult> = await resp.json()
for (const stat of data) {
switch (stat.name) {
case 'UniquePasswords':
password = stat.count
break
case 'UniqueUsernames':
username = stat.count
break
case 'UniqueIPs':
ip = stat.count
break
case 'TotalLoginAttempts':
attempts = stat.count
break
case 'UniqueCountries':
country = stat.count
break
}
}
return { password, username, ip, attempts, country }
}
}

467
frontend/src/js/app.tsx Normal file
View File

@ -0,0 +1,467 @@
import React from "react";
import { createRoot } from "react-dom/client";
import { useState, useEffect } from 'react';
import { ApiaryAPI, LoginAttempt, DummyApiaryAPIClient, ApiaryAPIClient, TotalStats, StatsResult, StatsType } from "./api";
import { BrowserRouter, NavLink, Routes, Route } from "react-router";
import { Chart as ChartJS, Tooltip, ArcElement, Legend, ChartOptions } from "chart.js";
import { Pie } from "react-chartjs-2";
import humanizeDuration from "humanize-duration";
import * as classes from "../css/style.module.css";
ChartJS.register(Tooltip, ArcElement, Legend);
console.log(classes);
interface AppProps {
api: ApiaryAPI
}
let chartColors = [
"#1abc9c",
"#2ecc71",
"#3498db",
"#9b59b6",
"#34495e",
"#16a085",
"#27ae60",
"#2980b9",
"#8e44ad",
"#2c3e50",
"#f1c40f",
"#e67e22",
"#e74c3c",
"#ecf0f1",
"#95a5a6",
"#f39c12",
"#d35400",
"#c0392b",
"#bdc3c7",
"#7f8c8d",
]
export function App({ api }: AppProps) {
const [mode, setMode] = useState("light");
const headerProps: HeaderMenuProps = {
title: "apiary.home.2rjus.net",
items: [
{
name: "Totals",
path: "/",
},
{
name: "Passwords",
path: "/stats/password",
},
{
name: "Usernames",
path: "/stats/username",
},
{
name: "IPs",
path: "/stats/ip",
},
{
name: "Live",
path: "/live",
},
{
name: "Query",
path: "/query",
}
]
}
const onSelectMode = (mode: string) => {
setMode(mode);
if (mode === "dark") {
document.body.classList.add(classes.dark_mode);
} else {
document.body.classList.remove(classes.dark_mode);
}
}
useEffect(() => {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => onSelectMode(e.matches ? "dark" : "light"));
onSelectMode(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
return () => {
window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', (e) => onSelectMode(e.matches ? "dark" : "light"));
}
}, [])
return (
<>
<BrowserRouter>
<div id="app">
<HeaderMenu title={headerProps.title} items={headerProps.items} />
<div className={classes.content}>
<Routes>
<Route path="/" element={<Home api={api} />} />
<Route path="/stats/password" element={<Stats api={api} type="password" />} />
<Route path="/stats/username" element={<Stats api={api} type="username" />} />
<Route path="/stats/ip" element={<Stats api={api} type="ip" />} />
<Route path="/live" element={<Live api={api} />} />
<Route path="/query" element={<Query api={api} />} />
</Routes>
</div>
</div>
</BrowserRouter >
</>
);
}
export interface StatsProps {
api: ApiaryAPI
type: StatsType
}
export function Stats({ api, type }: StatsProps) {
const [stats, setStats] = useState<StatsResult[] | null>(null)
const [currentType, setCurrentType] = useState(type)
useEffect(() => {
async function getStats() {
try {
let newStats = await api.stats(type, 10);
if (JSON.stringify(newStats) !== JSON.stringify(stats)) {
setStats(newStats)
}
} catch (e) {
console.log("Error getting stats", e)
}
}
if (currentType !== type) {
setCurrentType(type)
getStats()
}
if (stats === null) {
getStats()
}
const interval = setInterval(() => {
getStats()
}, 60000)
return () => {
clearInterval(interval);
}
}, [stats, type])
return (
<>
{(stats != null && stats.length > 0) ? <StatsPie data={stats} /> : <p>Loading...</p>}
</>
);
}
export interface StatsPieProps {
data: StatsResult[]
}
export function StatsPie({ data }: StatsPieProps) {
const labels = data.map((d) => d.name);
const values = data.map((d) => d.count);
const piedata = {
labels,
datasets: [{
label: "# of attempts",
data: values,
backgroundColor: chartColors,
borderWidth: 1
}]
};
console.log(piedata)
const getTextColor = () => {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'white' : 'black'
}
const options: ChartOptions<"pie"> = {
plugins: {
legend: {
display: true,
align: "start",
labels: {
color: getTextColor(),
}
}
}
}
return (
<div className={classes.stats_pie}>
<Pie data={piedata} options={options} />
</div>
)
}
export function Home({ api }: AppProps) {
const [totals, setTotals] = useState<TotalStats | null>(null)
useEffect(() => {
async function getTotals() {
let totals = await api.totals();
setTotals(totals);
}
if (!totals) {
getTotals();
}
const interval = setInterval(() => {
getTotals();
}, 5000)
return () => {
clearInterval(interval);
}
})
return (
<>
{totals ? <Totals totals={totals} /> : <p>Loading...</p>}
</>
);
}
export interface TotalsProps {
totals: TotalStats
}
export function Totals({ totals }: TotalsProps) {
return (
<div className={classes.totals}>
<div className={classes.totals_key}>
<h2>Unique passwords</h2>
<h2>Unique username</h2>
<h2>Unique IPs</h2>
<h2>Total attempts</h2>
</div>
<div className={classes.totals_value}>
<h2>{totals.password}</h2>
<h2>{totals.username}</h2>
<h2>{totals.ip}</h2>
<h2>{totals.attempts}</h2>
</div>
</div>
)
}
export function Live({ api }: AppProps) {
let list: LoginAttempt[] = [];
let [liveList, setLiveList] = useState(list);
useEffect(() => {
const cleanup = api.live((a) => {
setLiveList((list) => {
return [a, ...list];
});
});
return cleanup
}, [liveList, api]);
return (
<>
<LiveList list={liveList} />
</>
);
}
export interface LiveListProps {
list: LoginAttempt[]
};
export interface DateTDProps {
date: Date
now: Date
}
export function DateTD({ date, now }: DateTDProps) {
const [displayDate, setDisplayDate] = useState(date.toLocaleString());
useEffect(() => {
if (now.getTime() - date.getTime() < 14400000) {
const newDate = humanizeDuration(now.getTime() - date.getTime(), { largest: 1, round: true }) + " ago";
if (newDate !== displayDate) {
setDisplayDate(newDate);
}
}
}, [displayDate, now])
return (
<td className={classes.live_table_date}>{displayDate}</td>
)
}
export function LiveList({ list }: LiveListProps) {
const [now, setNow] = useState(new Date())
let items = list.map((a) => {
const attemptDate = new Date(a.date);
const key = `${a.username}-${a.password}-${a.remoteIP}-${a.date}`;
return (
<tr key={a.date}>
<DateTD date={attemptDate} now={now} />
<td className={classes.live_table_username}>{a.username}</td>
<td className={classes.live_table_password}>{a.password}</td>
<td className={classes.live_table_ip}>{a.remoteIP}</td>
<td className={classes.live_table_country}>{a.country}</td>
</tr>
)
})
useEffect(() => {
const interval = setInterval(() => {
setNow(new Date())
}, 1000)
return () => {
clearInterval(interval)
}
}, [now])
return (
<>
<table className={classes.live_table}>
<thead>
<tr>
<th className={classes.live_table_date}>Date</th>
<th className={classes.live_table_username}>Username</th>
<th className={classes.live_table_password}>Password</th>
<th className={classes.live_table_ip}>IP</th>
<th className={classes.live_table_country}>Country</th>
</tr>
</thead>
<tbody>
{items}
</tbody>
</table>
</>
)
};
export function Query({ api }: AppProps) {
const [liveList, setLiveList] = useState<LoginAttempt[]>([]);
const [queryErr, setQueryErr] = useState<Error | null>(null);
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const value = event.currentTarget.query.value;
if (value === "") {
setQueryErr(new Error("Query cannot be empty"));
return
}
try {
const results = await api.query("", value)
setQueryErr(null);
setLiveList(results);
} catch (e) {
if (e instanceof Error) {
setQueryErr(e);
}
}
}
return (
<>
<form onSubmit={handleSubmit}>
<input placeholder="Search..." name="query" type="text" />
</form>
{queryErr ? <ErrorBox message={queryErr.message} /> : null}
<LiveList list={liveList} />
</>
);
}
interface ErrorBoxProps {
message: string | null
};
export function ErrorBox({ message }: ErrorBoxProps) {
return (
<div className={classes.error_box}>
<p>Error: {message}</p>
</div>
)
}
export function Header() {
return (
<div className={classes.navbar}>
<h2 id="menu-title">apiary.home.2rjus.net</h2>
<nav className={classes.nav_flex}>
<ul>
<li><NavLink to="/" className={({ isActive }) => isActive ? classes.menu_link_active : classes.menu_link}>Home</NavLink></li>
<li><NavLink to="/stats/password" className={({ isActive }) => isActive ? classes.menu_link_active : classes.menu_link}>Stats</NavLink></li>
<li><NavLink to="/live" className={({ isActive }) => isActive ? classes.menu_link_active : classes.menu_link}>Live</NavLink></li>
<li><NavLink to="/query" className={({ isActive }) => isActive ? classes.menu_link_active : classes.menu_link}>Query</NavLink></li>
</ul>
</nav>
</div >
);
}
interface HeaderItem {
name: string
path: string
}
interface HeaderMenuProps {
title: string
items: Array<HeaderItem>
}
export function HeaderMenu({ title, items }: HeaderMenuProps) {
const menuItems = items.map((item) => {
return (
<li key={item.path}>
<NavLink to={item.path} className={({ isActive }) => isActive ? classes.menu_link_active : classes.menu_link}>{item.name}</NavLink>
</li>
)
})
return (
<div className={classes.navbar}>
<img id={classes.menu_logo}></img>
<h2 id={classes.menu_title}>{title}</h2>
<nav className={classes.nav_flex}>
<ul>
{menuItems}
</ul>
</nav>
</div>
)
}
export interface SubMenuProps {
items: Array<{ name: string, active: () => boolean, onClick: () => void }>
}
export function SubMenu({ items }: SubMenuProps) {
return (
<nav className={classes.submenu}>
<ul>
{items.map((item) => {
return <li>
<a
href="#"
className={item.active() ? classes.sub_menu_active : classes.sub_menu_link}
onClick={item.onClick}>{item.name}</a>
</li>
})}
</ul>
</nav>
)
}
const rootElement = document.getElementById('root');
if (rootElement) {
const root = createRoot(rootElement);
let api: ApiaryAPI;
if (process.env.NODE_ENV === "production") {
api = new ApiaryAPIClient();
} else {
api = new DummyApiaryAPIClient();
}
root.render(
<App api={api} />
);
}

View File

@ -0,0 +1,17 @@
import { DummyApiaryAPIClient, TotalStats } from "../js/api";
describe("DummyApiaryAPIClient", () => {
const api = new DummyApiaryAPIClient()
test("totals returns expeced value", async () => {
let totals = await api.totals()
const expected: TotalStats = {
password: 1,
username: 1,
ip: 1,
attempts: 1,
country: 1,
}
expect(totals).toEqual(expected)
})
});

16
frontend/tsconfig.json Normal file
View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"esModuleInterop": true,
"jsx": "react",
"module": "esnext",
"moduleResolution": "node",
"lib": ["dom", "esnext"],
"strict": true,
"sourceMap": true,
"target": "esnext",
"types": ["node", "jest"]
},
"exclude": ["node_modules"],
"include": ["src/**/*.ts", "src/**/*.tsx"]
}

67
go.mod
View File

@ -1,32 +1,49 @@
module git.t-juice.club/torjus/apiary module git.t-juice.club/torjus/apiary
go 1.16 go 1.24.0
toolchain go1.24.1
require ( require (
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac // indirect
github.com/emersion/go-smtp v0.15.0
github.com/fujiwara/shapeio v1.0.0 github.com/fujiwara/shapeio v1.0.0
github.com/gliderlabs/ssh v0.3.3 github.com/gliderlabs/ssh v0.3.8
github.com/go-chi/chi/v5 v5.0.4 github.com/go-chi/chi/v5 v5.2.1
github.com/golang/protobuf v1.5.2 // indirect github.com/google/uuid v1.6.0
github.com/google/uuid v1.3.0 github.com/jackc/pgx/v5 v5.7.2
github.com/jackc/pgx/v4 v4.13.0 github.com/oschwald/maxminddb-golang v1.13.1
github.com/oschwald/maxminddb-golang v1.8.0 github.com/pelletier/go-toml/v2 v2.2.3
github.com/pelletier/go-toml v1.9.4 github.com/prometheus/client_golang v1.21.1
github.com/prometheus/client_golang v1.11.0 github.com/urfave/cli/v3 v3.0.0-beta1
github.com/prometheus/common v0.32.1 // indirect golang.org/x/crypto v0.36.0
github.com/prometheus/procfs v0.7.3 // indirect )
github.com/urfave/cli/v2 v2.3.0
go.uber.org/atomic v1.9.0 // indirect require (
go.uber.org/multierr v1.7.0 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
go.uber.org/zap v1.19.1 github.com/beorn7/perks v1.0.1 // indirect
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 github.com/cespare/xxhash/v2 v2.3.0 // indirect
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6 // indirect github.com/icholy/gomajor v0.14.0 // indirect
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
golang.org/x/text v0.3.7 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
google.golang.org/protobuf v1.27.1 // indirect github.com/klauspost/compress v1.18.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.63.0 // indirect
github.com/prometheus/procfs v0.16.0 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.29.0 // indirect
golang.org/x/vuln v1.1.4 // indirect
google.golang.org/protobuf v1.36.5 // indirect
)
tool (
github.com/icholy/gomajor
golang.org/x/vuln/cmd/govulncheck
) )

694
go.sum
View File

@ -1,649 +1,93 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac h1:tn/OQ2PmwQ0XFVgAHfjlLyqMewry25Rz7jWnVoh4Ggs=
github.com/emersion/go-sasl v0.0.0-20211008083017-0b9dcfb154ac/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.15.0 h1:3+hMGMGrqP/lqd7qoxZc1hTU8LY8gHV9RFGWlqSDmP8=
github.com/emersion/go-smtp v0.15.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fujiwara/shapeio v1.0.0 h1:xG5D9oNqCSUUbryZ/jQV3cqe1v2suEjwPIcEg1gKM8M= github.com/fujiwara/shapeio v1.0.0 h1:xG5D9oNqCSUUbryZ/jQV3cqe1v2suEjwPIcEg1gKM8M=
github.com/fujiwara/shapeio v1.0.0/go.mod h1:LmEmu6L/8jetyj1oewewFb7bZCNRwE7wLCUNzDLaLVA= github.com/fujiwara/shapeio v1.0.0/go.mod h1:LmEmu6L/8jetyj1oewewFb7bZCNRwE7wLCUNzDLaLVA=
github.com/gliderlabs/ssh v0.3.3 h1:mBQ8NiOgDkINJrZtoizkC3nDNYgSaWtxyem6S2XHBtA= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.3/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-chi/chi/v5 v5.0.4 h1:5e494iHzsYBiyXQAHHuI4tyJS9M3V84OuX3ufIIGHFo= github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.0.4/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786 h1:rcv+Ippz6RAtvaGgKxc+8FQIpxHgsF+HBzPyYL2cyVU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
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/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/icholy/gomajor v0.14.0 h1:qr0eGSMyLcZa7lmSuiplH3kr6j4oH6ET8nkdZqpDcks=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/icholy/gomajor v0.14.0/go.mod h1:oJuk5dppdmnvUQRKsUNelWZ7KmYpvYKVupt/OQ0y9UI=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.10.0 h1:4EYhlDVEMsJ30nNj0mmgwIUXoq7e9sMJrVC2ED6QlCU=
github.com/jackc/pgconn v1.10.0/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/jackc/pgproto3/v2 v2.1.1 h1:7PQ/4gLoqnl87ZxL7xjO0DR5gYuviDCZxQJsUlFW1eI= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/jackc/pgtype v1.8.1 h1:9k0IXtdJXHJbyAWQgbWr1lU+MEhPXZz6RIXxfR5oxXs= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/jackc/pgtype v1.8.1/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.13.0 h1:JCjhT5vmhMAf/YwBHLvrBn4OGdIQBiFG6ym8Zmdx570=
github.com/jackc/pgx/v4 v4.13.0/go.mod h1:9P4X524sErlaxj0XSGZk7s+LD0eOyu1ZDUrrpznYDF0=
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=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
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=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oschwald/maxminddb-golang v1.8.0 h1:Uh/DSnGoxsyp/KYbY1AuP0tYEwfs0sCph9p/UMXK/Hk=
github.com/oschwald/maxminddb-golang v1.8.0/go.mod h1:RXZtst0N6+FY/3qCNmZMBApR19cdQj43/NM9VkrNAis=
github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjcw9Zg=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7 h1:FemxDzfMUcK2f3YY4H+05K9CDzbSVr2+q/JKN45pey0=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec=
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI=
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6 h1:Z04ewVs7JhXaYkmDhBERPi41gnltfQpMWDnTnQbaCqk=
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
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=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
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=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 h1:2B5p2L5IfGiD7+b9BOoRMC6DgObAVZV+Fsp050NqXik=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
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=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
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=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
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=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
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=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
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.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@ -1,71 +0,0 @@
package ports
import (
"database/sql"
"time"
_ "github.com/jackc/pgx/v4/stdlib"
)
const DBSchema = `
CREATE TABLE IF NOT EXISTS port_attempts(
id serial PRIMARY KEY,
date timestamptz,
remote_ip inet,
port varchar(5)
);`
type PostgresStore struct {
db *sql.DB
}
func NewPostgresStore(dsn string) (*PostgresStore, error) {
db, err := sql.Open("pgx", dsn)
if err != nil {
return nil, err
}
s := &PostgresStore{db: db}
return s, nil
}
func (s *PostgresStore) InitDB() error {
_, err := s.db.Exec(DBSchema)
return err
}
func (s *PostgresStore) Add(attempt *ConnectionAttempt) error {
stmt := `INSERT INTO port_attempts(date, remote_ip, port)
VALUES ($1, $2, $3)`
tx, err := s.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
_, err = tx.Exec(stmt, time.Now(), attempt.From, attempt.Port)
if err != nil {
return err
}
return tx.Commit()
}
func (s *PostgresStore) List() ([]*ConnectionAttempt, error) {
stmt := `SELECT remote_ip, port FROM port_attempts`
rows, err := s.db.Query(stmt)
if err != nil {
return nil, err
}
defer rows.Close()
var attempts []*ConnectionAttempt
for rows.Next() {
var a ConnectionAttempt
if err := rows.Scan(&a.From, &a.Port); err != nil {
return nil, err
}
attempts = append(attempts, &a)
}
return attempts, nil
}

View File

@ -1,96 +0,0 @@
package ports
import (
"context"
"fmt"
"net"
"time"
"go.uber.org/zap"
)
type Server struct {
EnabledPortsTCP []string
EnabledPortsUDP []string
IP string
Logger *zap.SugaredLogger
store Store
}
func New(store Store) *Server {
return &Server{store: store, Logger: zap.NewNop().Sugar()}
}
func (s *Server) Start(ctx context.Context) error {
for _, port := range s.EnabledPortsTCP {
portCtx, cancel := context.WithCancel(ctx)
go s.doListenTCP(portCtx, port)
defer cancel()
}
<-ctx.Done()
return nil
}
func (s *Server) AddTCPPort(port string) {
s.EnabledPortsTCP = append(s.EnabledPortsTCP, port)
}
func (s *Server) doListenTCP(ctx context.Context, port string) error {
lAddr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort(s.IP, port))
if err != nil {
s.Logger.Warnw("Error resoling listening addr.", "network", "tcp", "port", port)
return fmt.Errorf("error resolving listening address: %w", err)
}
listener, err := net.ListenTCP("tcp", lAddr)
if err != nil {
return fmt.Errorf("error starting listener: %w", err)
}
defer listener.Close()
go func() {
<-ctx.Done()
s.Logger.Debug("Listener context cancelled.")
listener.Close()
}()
s.Logger.Infow("Listening for connections to TCP port.", "port", port)
for {
conn, err := listener.Accept()
if err != nil {
select {
case <-ctx.Done():
s.Logger.Infow("Listener shutting down.", "port", port, "network", "tcp")
return nil
default:
s.Logger.Infow("Error accepting connection.", "error", err)
continue
}
}
s.Logger.Infow("Got connection on port.", "port", port, "network", "tcp", "remote_addr", conn.RemoteAddr().String())
conn.SetReadDeadline(time.Now().Add(time.Second * 15))
buf := make([]byte, 256)
_, err = conn.Read(buf)
if err != nil {
buf = []byte{}
}
attempt := &ConnectionAttempt{
Port: port,
Network: "tcp",
From: conn.RemoteAddr().String(),
Data: buf,
}
conn.Close()
if err := s.store.Add(attempt); err != nil {
s.Logger.Warnw("Error storing attempt in store.", "error", err)
}
}
}

View File

@ -1,45 +0,0 @@
package ports_test
import (
"net"
"testing"
"time"
"context"
"git.t-juice.club/torjus/apiary/honeypot/ports"
)
func TestServer(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
store := &ports.MemoryStore{}
server := ports.New(store)
server.IP = "127.0.0.1"
server.AddTCPPort("25")
go server.Start(ctx)
rAddr, err := net.ResolveTCPAddr("tcp", net.JoinHostPort(server.IP, "25"))
if err != nil {
t.Fatalf("Error resolving remote address: %s", err)
}
conn, err := net.DialTCP("tcp", nil, rAddr)
if err != nil {
t.Fatalf("Dialing server returned error: %s", err)
}
conn.Write([]byte("LOL"))
conn.Close()
time.Sleep(1 * time.Second)
cancel()
attempts, err := store.List()
if err != nil {
t.Fatalf("Getting attempts from store returned error: %s", err)
}
if len(attempts) != 1 {
t.Fatalf("Wrong amount of attempts in store: %d", len(attempts))
}
}

View File

@ -1,26 +0,0 @@
package ports
type ConnectionAttempt struct {
Port string
Network string
From string
Data []byte
}
type Store interface {
Add(attempt *ConnectionAttempt) error
List() ([]*ConnectionAttempt, error)
}
type MemoryStore struct {
data []*ConnectionAttempt
}
func (s *MemoryStore) Add(attempt *ConnectionAttempt) error {
s.data = append(s.data, attempt)
return nil
}
func (s *MemoryStore) List() ([]*ConnectionAttempt, error) {
return s.data, nil
}

View File

@ -1,12 +0,0 @@
package smtp
import "time"
type MailData struct {
From string
Recipients []string
Data string
RemoteAddr string
Timestamp time.Time
}

View File

@ -1,30 +0,0 @@
package smtp
import "github.com/prometheus/client_golang/prometheus"
type MetricsStore struct {
backend SMTPStore
mailTotal *prometheus.CounterVec
}
func NewMetricsStore(backend SMTPStore) *MetricsStore {
store := &MetricsStore{backend: backend}
store.mailTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "apiary_smtp_received_total",
Help: "Total count of received emails.",
ConstLabels: prometheus.Labels{"service": "honeypot_smtp"},
},
[]string{"from"},
)
prometheus.MustRegister(store.mailTotal)
return store
}
func (s *MetricsStore) AddMailData(data *MailData) error {
s.mailTotal.WithLabelValues(data.From).Inc()
return s.backend.AddMailData(data)
}

View File

@ -1,124 +0,0 @@
package smtp
import (
"bytes"
"io"
"net/mail"
"time"
smtplib "github.com/emersion/go-smtp"
"go.uber.org/zap"
)
type backend struct {
honeypot *SMTPHoneypot
}
func (b *backend) Login(state *smtplib.ConnectionState, username, password string) (smtplib.Session, error) {
sess := &session{
honeypot: b.honeypot,
loginUsername: username,
loginPassword: password,
remoteAddr: state.RemoteAddr.String(),
}
return sess, nil
}
func (b *backend) AnonymousLogin(state *smtplib.ConnectionState) (smtplib.Session, error) {
sess := &session{
loginAnonymous: true,
remoteAddr: state.RemoteAddr.String(),
}
return sess, nil
}
type session struct {
honeypot *SMTPHoneypot
loginUsername string
loginPassword string
loginAnonymous bool
remoteAddr string
mail MailData
}
func (sess *session) Reset() {
}
func (sess *session) Logout() error {
return nil
}
func (sess *session) Mail(from string, opts smtplib.MailOptions) error {
sess.mail.From = from
return nil
}
func (sess *session) Rcpt(to string) error {
sess.mail.Recipients = append(sess.mail.Recipients, to)
return nil
}
func (sess *session) Data(r io.Reader) error {
lr := io.LimitReader(r, 1024*1024)
var buf bytes.Buffer
n, err := io.Copy(&buf, lr)
if err != nil {
sess.honeypot.Logger.Debugw("Error reading email data from client.", "error", err, "remote_addr", sess.remoteAddr)
}
if n == 1024*1024 {
// There is probably more data, read the rest from the client
// We don't really care what happens to this, or if it errors
_, _ = io.Copy(io.Discard, r)
}
sess.mail.Data = buf.String()
var toAddr string
var subject string
// Parse mail headers
msg, err := mail.ReadMessage(&buf)
if err != nil {
sess.honeypot.Logger.Infow("Unable to parse mail.", "error", err, "remote_addr", sess.remoteAddr)
} else {
toAddr = msg.Header.Get("To")
subject = msg.Header.Get("Subject")
}
if err := sess.honeypot.Store.AddMailData(&sess.mail); err != nil {
sess.honeypot.Logger.Warnw("Error storing maildata.", "error", err, "remote_addr", sess.remoteAddr)
return smtplib.ErrDataTooLarge
}
sess.honeypot.Logger.Infow("Mail received", "from", sess.mail.From, "to", toAddr, "subject", subject, "remote_addr", sess.remoteAddr, "size", len(sess.mail.Data))
return nil
}
type SMTPHoneypot struct {
Addr string
Logger *zap.SugaredLogger
Store SMTPStore
server *smtplib.Server
}
func NewSMTPHoneypot() (*SMTPHoneypot, error) {
honeypot := &SMTPHoneypot{
Logger: zap.NewNop().Sugar(),
}
return honeypot, nil
}
func (sh *SMTPHoneypot) ListenAndServe() error {
server := smtplib.NewServer(&backend{
honeypot: sh,
})
server.Addr = sh.Addr
server.Domain = "apiary.t-juice.club"
server.AllowInsecureAuth = true
server.ReadTimeout = 10 * time.Second
server.WriteTimeout = 10 * time.Second
sh.server = server
return server.ListenAndServe()
}
func (sh *SMTPHoneypot) Shutdown() error {
return sh.server.Close()
}

View File

@ -1,12 +0,0 @@
package smtp
type SMTPStore interface {
AddMailData(mail *MailData) error
}
type DiscardStore struct {
}
func (s *DiscardStore) AddMailData(mail *MailData) error {
return nil
}

View File

@ -1,11 +0,0 @@
package ssh
type ActionType int
const (
ActionTypeLogPassword ActionType = iota
ActionTypeLogPasswordSlow
ActionTypeLogCommandAndExit
ActionTypeSendGarbage
)
const ActionTypeDefault ActionType = ActionTypeLogPassword

View File

@ -13,7 +13,7 @@ var mmdb []byte
func (s *HoneypotServer) LookupCountry(ip net.IP) string { func (s *HoneypotServer) LookupCountry(ip net.IP) string {
db, err := maxminddb.FromBytes(mmdb) db, err := maxminddb.FromBytes(mmdb)
if err != nil { if err != nil {
s.Logger.Warnw("Error opening geoip database", "error", err) s.Logger.Warn("Error opening geoip database", "error", err)
return "??" return "??"
} }
@ -25,7 +25,7 @@ func (s *HoneypotServer) LookupCountry(ip net.IP) string {
err = db.Lookup(ip, &record) err = db.Lookup(ip, &record)
if err != nil { if err != nil {
s.Logger.Warnw("Error doing geoip lookup", "error", err) s.Logger.Warn("Error doing geoip lookup", "error", err)
return "??" return "??"
} }
if record.Country.ISOCode == "None" { if record.Country.ISOCode == "None" {

View File

@ -3,6 +3,7 @@ package ssh
import ( import (
"context" "context"
"io" "io"
"log/slog"
"net" "net"
"os" "os"
"time" "time"
@ -16,11 +17,10 @@ import (
"git.t-juice.club/torjus/apiary/models" "git.t-juice.club/torjus/apiary/models"
sshlib "github.com/gliderlabs/ssh" sshlib "github.com/gliderlabs/ssh"
"github.com/google/uuid" "github.com/google/uuid"
"go.uber.org/zap"
) )
type HoneypotServer struct { type HoneypotServer struct {
Logger *zap.SugaredLogger Logger *slog.Logger
sshServer *sshlib.Server sshServer *sshlib.Server
@ -33,7 +33,7 @@ type HoneypotServer struct {
func NewHoneypotServer(cfg config.HoneypotConfig, store store.LoginAttemptStore) (*HoneypotServer, error) { func NewHoneypotServer(cfg config.HoneypotConfig, store store.LoginAttemptStore) (*HoneypotServer, error) {
var hs HoneypotServer var hs HoneypotServer
hs.attemptStore = store hs.attemptStore = store
hs.Logger = zap.NewNop().Sugar() hs.Logger = slog.New(slog.NewTextHandler(io.Discard, nil))
hs.sshServer = &sshlib.Server{ hs.sshServer = &sshlib.Server{
Addr: cfg.ListenAddr, Addr: cfg.ListenAddr,
@ -60,6 +60,8 @@ func NewHoneypotServer(cfg config.HoneypotConfig, store store.LoginAttemptStore)
hs.sshServer.AddHostKey(signer) hs.sshServer.AddHostKey(signer)
} }
hs.throttleSpeed = 1024 * 1024
return &hs, nil return &hs, nil
} }
@ -92,19 +94,19 @@ func (hs *HoneypotServer) passwordHandler(ctx sshlib.Context, password string) b
} }
country := hs.LookupCountry(la.RemoteIP) country := hs.LookupCountry(la.RemoteIP)
if utf8.RuneCountInString(country) > 2 { if utf8.RuneCountInString(country) > 2 {
hs.Logger.Warnw("Too many characters in country", "country", country, "runecount", utf8.RuneCountInString(country)) hs.Logger.Warn("Too many characters in country", "country", country, "runecount", utf8.RuneCountInString(country))
country = "??" country = "??"
} }
la.Country = country la.Country = country
hs.Logger.Infow("Login attempt", hs.Logger.Info("Login attempt",
"remote_ip", la.RemoteIP.String(), "remote_ip", la.RemoteIP.String(),
"username", la.Username, "username", la.Username,
"password", la.Password, "password", la.Password,
"country", la.Country) "country", la.Country)
if err := hs.attemptStore.AddAttempt(&la); err != nil { if err := hs.attemptStore.AddAttempt(&la); err != nil {
hs.Logger.Warnw("Error adding attempt to store", "error", err) hs.Logger.Warn("Error adding attempt to store", "error", err)
} }
for _, cFunc := range hs.attemptsCallbacks { for _, cFunc := range hs.attemptsCallbacks {
@ -115,16 +117,17 @@ func (hs *HoneypotServer) passwordHandler(ctx sshlib.Context, password string) b
} }
func (s *HoneypotServer) connCallback(ctx sshlib.Context, conn net.Conn) net.Conn { func (s *HoneypotServer) connCallback(ctx sshlib.Context, conn net.Conn) net.Conn {
s.Logger.Debug("Connection received.", "remote_addr", conn.RemoteAddr())
throttledConn := newThrottledConn(conn) throttledConn := newThrottledConn(conn)
throttledConn.SetSpeed(s.throttleSpeed) throttledConn.SetSpeed(s.throttleSpeed)
ctx.SetValue("uuid", throttledConn.ID) ctx.SetValue("uuid", throttledConn.ID)
throttledConn.SetSpeed(s.throttleSpeed)
return throttledConn return throttledConn
} }
func handler(session sshlib.Session) { func handler(session sshlib.Session) {
_, _ = io.WriteString(session, "[root@hostname ~]#") _, _ = io.WriteString(session, "[root@hostname ~]#")
session.Exit(1) _ = session.Exit(1)
} }
func ipFromAddr(addr string) net.IP { func ipFromAddr(addr string) net.IP {

View File

@ -2,6 +2,8 @@ package store
import "git.t-juice.club/torjus/apiary/models" import "git.t-juice.club/torjus/apiary/models"
var _ LoginAttemptStore = &CachingStore{}
type CachingStore struct { type CachingStore struct {
backend LoginAttemptStore backend LoginAttemptStore
@ -33,14 +35,16 @@ func NewCachingStore(backend LoginAttemptStore) *CachingStore {
//TODO: Handle better maybe? //TODO: Handle better maybe?
panic(err) panic(err)
} }
var loginCount int
cs.totalLoginsCount = len(all) for attempt := range all {
for _, attempt := range all {
cs.uniqueUsernames[attempt.Username] = struct{}{} cs.uniqueUsernames[attempt.Username] = struct{}{}
cs.uniquePasswords[attempt.Password] = struct{}{} cs.uniquePasswords[attempt.Password] = struct{}{}
cs.uniqueIPs[attempt.RemoteIP.String()] = struct{}{} cs.uniqueIPs[attempt.RemoteIP.String()] = struct{}{}
cs.uniqueCountries[attempt.Country] = struct{}{} cs.uniqueCountries[attempt.Country] = struct{}{}
loginCount++
} }
cs.totalLoginsCount = loginCount
return cs return cs
} }
@ -58,7 +62,7 @@ func (s *CachingStore) AddAttempt(l *models.LoginAttempt) error {
return s.backend.AddAttempt(l) return s.backend.AddAttempt(l)
} }
func (s *CachingStore) All() ([]models.LoginAttempt, error) { func (s *CachingStore) All() (<-chan models.LoginAttempt, error) {
return s.backend.All() return s.backend.All()
} }

View File

@ -9,6 +9,8 @@ import (
"git.t-juice.club/torjus/apiary/models" "git.t-juice.club/torjus/apiary/models"
) )
var _ LoginAttemptStore = &MemoryStore{}
type MemoryStore struct { type MemoryStore struct {
lock sync.RWMutex lock sync.RWMutex
attempts []models.LoginAttempt attempts []models.LoginAttempt
@ -32,8 +34,17 @@ func (ms *MemoryStore) AddAttempt(l *models.LoginAttempt) error {
return nil return nil
} }
func (ms *MemoryStore) All() ([]models.LoginAttempt, error) { func (ms *MemoryStore) All() (<-chan models.LoginAttempt, error) {
return ms.attempts, nil ch := make(chan models.LoginAttempt)
go func() {
ms.lock.RLock()
defer ms.lock.RUnlock()
for _, attempt := range ms.attempts {
ch <- attempt
}
close(ch)
}()
return ch, nil
} }
func (ms *MemoryStore) Stats(statType LoginStats, limit int) ([]StatsResult, error) { func (ms *MemoryStore) Stats(statType LoginStats, limit int) ([]StatsResult, error) {

View File

@ -10,6 +10,7 @@ func TestMemoryStore(t *testing.T) {
s := &store.MemoryStore{} s := &store.MemoryStore{}
testLoginAttemptStore(s, t) testLoginAttemptStore(s, t)
} }
func TestMemoryStoreWithCache(t *testing.T) { func TestMemoryStoreWithCache(t *testing.T) {
backend := &store.MemoryStore{} backend := &store.MemoryStore{}
s := store.NewCachingStore(backend) s := store.NewCachingStore(backend)
@ -22,3 +23,8 @@ func BenchmarkMemoryStore(b *testing.B) {
} }
benchmarkLoginAttemptStore(setupFunc, b) benchmarkLoginAttemptStore(setupFunc, b)
} }
func FuzzMemoryStore(f *testing.F) {
s := &store.MemoryStore{}
fuzzLoginAttemptStore(s, f)
}

View File

@ -8,6 +8,8 @@ import (
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
) )
var _ LoginAttemptStore = &MetricsCollectingStore{}
const tickDuration = 5 * time.Second const tickDuration = 5 * time.Second
type MetricsCollectingStore struct { type MetricsCollectingStore struct {
@ -75,7 +77,7 @@ func NewMetricsCollectingStore(ctx context.Context, store LoginAttemptStore) *Me
case <-ctx.Done(): case <-ctx.Done():
return return
case <-mcs.statsTicker.C: case <-mcs.statsTicker.C:
mcs.Stats(LoginStatsTotals, 0) mcs.Stats(LoginStatsTotals, 0) // nolint: errcheck
} }
} }
}() }()
@ -89,7 +91,7 @@ func (s *MetricsCollectingStore) AddAttempt(l *models.LoginAttempt) error {
return err return err
} }
func (s *MetricsCollectingStore) All() ([]models.LoginAttempt, error) { func (s *MetricsCollectingStore) All() (<-chan models.LoginAttempt, error) {
return s.store.All() return s.store.All()
} }
@ -114,7 +116,6 @@ func (s *MetricsCollectingStore) Stats(statType LoginStats, limit int) ([]StatsR
} }
} }
return stats, err return stats, err
} }
func (s *MetricsCollectingStore) Query(query AttemptQuery) ([]models.LoginAttempt, error) { func (s *MetricsCollectingStore) Query(query AttemptQuery) ([]models.LoginAttempt, error) {

View File

@ -6,9 +6,11 @@ import (
"net" "net"
"git.t-juice.club/torjus/apiary/models" "git.t-juice.club/torjus/apiary/models"
_ "github.com/jackc/pgx/v4/stdlib" _ "github.com/jackc/pgx/v5/stdlib"
) )
var _ LoginAttemptStore = &PostgresStore{}
const DBSchema = ` const DBSchema = `
CREATE TABLE IF NOT EXISTS login_attempts( CREATE TABLE IF NOT EXISTS login_attempts(
id serial PRIMARY KEY, id serial PRIMARY KEY,
@ -51,7 +53,7 @@ RETURNING id;`
if err != nil { if err != nil {
return err return err
} }
defer tx.Rollback() defer tx.Rollback() // nolint: errcheck
var id int var id int
if err := tx.QueryRow(stmt, l.Date, l.RemoteIP.String(), l.Username, l.Password, l.SSHClientVersion, l.ConnectionUUID, l.Country).Scan(&id); err != nil { if err := tx.QueryRow(stmt, l.Date, l.RemoteIP.String(), l.Username, l.Password, l.SSHClientVersion, l.ConnectionUUID, l.Country).Scan(&id); err != nil {
@ -61,27 +63,30 @@ RETURNING id;`
return tx.Commit() return tx.Commit()
} }
func (s *PostgresStore) All() ([]models.LoginAttempt, error) { func (s *PostgresStore) All() (<-chan models.LoginAttempt, error) {
stmt := `SELECT date, remote_ip, username, password, client_version, connection_uuid, country FROM login_attempts` stmt := `SELECT date, remote_ip, username, password, client_version, connection_uuid, country FROM login_attempts`
rows, err := s.db.Query(stmt) rows, err := s.db.Query(stmt)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close()
var attempts []models.LoginAttempt ch := make(chan models.LoginAttempt)
for rows.Next() { go func() {
var a models.LoginAttempt defer rows.Close()
var ip string for rows.Next() {
if err := rows.Scan(&a.Date, &ip, &a.Username, &a.Password, &a.SSHClientVersion, &a.SSHClientVersion, &a.Country); err != nil { var a models.LoginAttempt
return nil, err var ip string
if err := rows.Scan(&a.Date, &ip, &a.Username, &a.Password, &a.SSHClientVersion, &a.SSHClientVersion, &a.Country); err != nil {
panic(err)
}
a.RemoteIP = net.ParseIP(ip)
ch <- a
} }
a.RemoteIP = net.ParseIP(ip) close(ch)
attempts = append(attempts, a) }()
}
return attempts, nil return ch, nil
} }
func (s *PostgresStore) Stats(statType LoginStats, limit int) ([]StatsResult, error) { func (s *PostgresStore) Stats(statType LoginStats, limit int) ([]StatsResult, error) {

View File

@ -1,3 +1,4 @@
// nolint: errcheck
package store_test package store_test
import ( import (
@ -6,6 +7,7 @@ import (
"testing" "testing"
"git.t-juice.club/torjus/apiary/honeypot/ssh/store" "git.t-juice.club/torjus/apiary/honeypot/ssh/store"
_ "github.com/jackc/pgx/v5/stdlib"
) )
func TestPostgresStore(t *testing.T) { func TestPostgresStore(t *testing.T) {
@ -27,6 +29,7 @@ func TestPostgresStore(t *testing.T) {
testLoginAttemptStore(s, t) testLoginAttemptStore(s, t)
} }
func TestPostgresStoreWithCache(t *testing.T) { func TestPostgresStoreWithCache(t *testing.T) {
var dsn string var dsn string
var found bool var found bool
@ -47,6 +50,7 @@ func TestPostgresStoreWithCache(t *testing.T) {
testLoginAttemptStore(s, t) testLoginAttemptStore(s, t)
} }
func BenchmarkPostgresStore(b *testing.B) { func BenchmarkPostgresStore(b *testing.B) {
var dsn string var dsn string
var found bool var found bool

View File

@ -38,7 +38,7 @@ type AttemptQuery struct {
} }
type LoginAttemptStore interface { type LoginAttemptStore interface {
AddAttempt(l *models.LoginAttempt) error AddAttempt(l *models.LoginAttempt) error
All() ([]models.LoginAttempt, error) All() (<-chan models.LoginAttempt, error)
Stats(statType LoginStats, limit int) ([]StatsResult, error) Stats(statType LoginStats, limit int) ([]StatsResult, error)
Query(query AttemptQuery) ([]models.LoginAttempt, error) Query(query AttemptQuery) ([]models.LoginAttempt, error)
HealthCheker HealthCheker

View File

@ -25,8 +25,12 @@ func testLoginAttemptStore(s store.LoginAttemptStore, t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Error getting all attempts: %s", err) t.Fatalf("Error getting all attempts: %s", err)
} }
if len(all) != len(testAttempts) { var count int
t.Errorf("All returned wrong amount. Got %d want %d", len(all), len(testAttempts)) for range all {
count++
}
if count != len(testAttempts) {
t.Errorf("All returned wrong amount. Got %d want %d", count, len(testAttempts))
} }
stats, err := s.Stats(store.LoginStatsTotals, 1) stats, err := s.Stats(store.LoginStatsTotals, 1)
if err != nil { if err != nil {
@ -229,7 +233,11 @@ func benchmarkLoginAttemptStore(setupFunc func() store.LoginAttemptStore, b *tes
if err != nil { if err != nil {
b.Fatalf("Error fetchin all: %s", err) b.Fatalf("Error fetchin all: %s", err)
} }
_ = len(all) var count int
for range all {
count++
}
_ = count
} }
}) })
} }
@ -315,3 +323,22 @@ func equalAttempts(a, b []models.LoginAttempt) bool {
return true return true
} }
func fuzzLoginAttemptStore(s store.LoginAttemptStore, f *testing.F) {
usernames := []string{"username", "root", "ubnt", "pi", "admin"}
for _, username := range usernames {
f.Add(username)
}
f.Fuzz(func(t *testing.T, orig string) {
attempt := models.LoginAttempt{
Date: time.Now(),
Username: orig,
Password: randomString(8),
Country: "NO",
}
if err := s.AddAttempt(&attempt); err != nil {
t.Fatalf("error adding: %s", err)
}
})
}

View File

@ -1,32 +0,0 @@
package models
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
var ErrInvalidPassword = fmt.Errorf("invalid password")
type User struct {
Username string `json:"username"`
PasswordHash []byte `json:"passwordHash"`
}
func (u *User) SetPassword(password string) error {
newHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("error hashing new password: %w", err)
}
u.PasswordHash = newHash
return nil
}
func (u *User) VerifyPassword(password string) error {
err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password))
if err != nil {
return ErrInvalidPassword
}
return nil
}

View File

@ -6,7 +6,7 @@ import (
) )
var ( var (
Version = "v0.1.27" Version = "v0.2.4"
Build string Build string
) )

View File

@ -1,3 +1,4 @@
//go:build !embed
// +build !embed // +build !embed
package web package web
@ -21,7 +22,7 @@ func (s *Server) IndexHandler(frontendDir string) http.HandlerFunc {
fname := filepath.Join(frontendDir, r.URL.Path) fname := filepath.Join(frontendDir, r.URL.Path)
_, err := os.Stat(fname) _, err := os.Stat(fname)
if os.IsNotExist(err) { if os.IsNotExist(err) {
serveIndex(frontendDir).ServeHTTP(w, r) s.serveIndex(frontendDir).ServeHTTP(w, r)
return return
} }
fs.ServeHTTP(w, r) fs.ServeHTTP(w, r)
@ -29,7 +30,7 @@ func (s *Server) IndexHandler(frontendDir string) http.HandlerFunc {
return http.HandlerFunc(fn) return http.HandlerFunc(fn)
} }
func serveIndex(frontendDir string) http.HandlerFunc { func (s *Server) serveIndex(frontendDir string) http.HandlerFunc {
fn := func(w http.ResponseWriter, r *http.Request) { fn := func(w http.ResponseWriter, r *http.Request) {
fname := filepath.Join(frontendDir, "index.html") fname := filepath.Join(frontendDir, "index.html")
f, err := os.Open(fname) f, err := os.Open(fname)
@ -38,7 +39,9 @@ func serveIndex(frontendDir string) http.HandlerFunc {
} }
defer f.Close() defer f.Close()
io.Copy(w, f) if _, err := io.Copy(w, f); err != nil {
s.ServerLogger.Warn("Error writing frontend to client.", "err", err)
}
} }
return fn return fn
} }

View File

@ -1,3 +0,0 @@
> 1%
last 2 versions
not dead

View File

@ -1,7 +0,0 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 100

View File

@ -1,20 +0,0 @@
module.exports = {
root: true,
env: {
node: true,
},
extends: [
'plugin:vue/essential',
'@vue/airbnb',
'@vue/typescript/recommended',
'prettier'
],
parserOptions: {
ecmaVersion: 2020,
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'implicit-arrow-linebreak': 'warn',
},
};

View File

@ -1,23 +0,0 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -1,24 +0,0 @@
# vue-frontend
## Project setup
```
yarn install
```
### Compiles and hot-reloads for development
```
yarn serve
```
### Compiles and minifies for production
```
yarn build
```
### Lints and fixes files
```
yarn lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

View File

@ -1,5 +0,0 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset',
],
};

View File

@ -1,50 +0,0 @@
{
"name": "apiary-frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@fontsource/rubik": "^4.2.2",
"@fontsource/secular-one": "^4.2.2",
"axios": "^0.21.1",
"bootstrap": "^4.6.0",
"bootstrap-vue": "^2.21.2",
"chart.js": "^3.0.2",
"core-js": "^3.6.5",
"d3": "^6.6.2",
"d3-svg-legend": "^2.25.6",
"faker": "^5.5.2",
"randomcolor": "^0.6.2",
"s-ago": "^2.2.0",
"vue": "^2.6.11",
"vue-class-component": "^7.2.3",
"vue-property-decorator": "^9.1.2",
"vue-router": "^3.5.1"
},
"devDependencies": {
"@types/chart.js": "^2.9.32",
"@types/d3": "^6.3.0",
"@types/faker": "^5.5.1",
"@types/randomcolor": "^0.5.5",
"@typescript-eslint/eslint-plugin": "^4.18.0",
"@typescript-eslint/parser": "^4.18.0",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-typescript": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/eslint-config-airbnb": "^5.0.2",
"@vue/eslint-config-typescript": "^7.0.0",
"eslint": "^6.7.2",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-vue": "^6.2.2",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",
"typescript": "~4.1.5",
"vue-template-compiler": "^2.6.11"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,22 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="description" content="Stats about logins, usernames and passwords received by the SSH honeypot.">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>apiary.t-juice.club</title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -1,75 +0,0 @@
<template>
<div id="app">
<div id="apiary-navbar">
<b-navbar toggleable="md" type="dark" variant="dark">
<b-navbar-brand href="#">
<img
alt="logo"
src="logo192.png"
width="29"
height="29"
class="d-inline-block align-top"
/>
apiary.t-juice.club
</b-navbar-brand>
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
<b-collapse id="nav-collapse" is-nav>
<b-navbar-nav class="mr-auto">
<b-nav-item :to="'/'">Home</b-nav-item>
<b-nav-item :to="'/stats'">Stats</b-nav-item>
<b-nav-item :to="'/attempts'">Attempts</b-nav-item>
<b-nav-item :to="'/search'">Search</b-nav-item>
</b-navbar-nav>
</b-collapse>
</b-navbar>
</div>
<b-container id="main-content-container" fluid="md">
<router-view></router-view>
</b-container>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import AttemptList from '@/components/AttemptList.vue';
import Stats from '@/components/Stats.vue';
import Home from '@/components/Home.vue';
@Component({
components: {
AttemptList,
Stats,
Home,
},
})
export default class App extends Vue {}
</script>
<style lang="scss">
$font-stack-header: 'Secular One', sans-serif;
$font-stack-content: 'Rubik', sans-serif;
#app {
font-family: $font-stack-content;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 0;
}
.navbar-brand {
font-family: $font-stack-header;
}
.navbar-nav a {
font-family: $font-stack-content;
}
h1 h2 h3 {
font-family: $font-stack-header;
}
#main-content-container {
margin-top: 50px;
text-align: center;
display: inline-block;
}
</style>

View File

@ -1,8 +0,0 @@
import { ApiaryAPI, LoginAttempt } from "@/apiary/apiary";
export class ApiaryDummyClient implements ApiaryAPI {
streamLoginAttempts(): any {
return 'a';
}
}

View File

@ -1,35 +0,0 @@
import FakerStatic from 'faker';
export interface LoginAttempt {
readonly date: string;
readonly remoteIP: string;
readonly username: string;
readonly password: string;
readonly sshClientVersion: string;
readonly connectionUUID: string;
}
export interface StatResult {
name: string;
count: number;
}
export interface ApiaryAPI {
streamLoginAttempts(): ReadableStream<LoginAttempt>;
}
export function fakeAttempt(): LoginAttempt {
return {
date: FakerStatic.date.recent().toLocaleDateString(),
remoteIP: FakerStatic.internet.ip(),
username: FakerStatic.internet.userName(),
password: FakerStatic.internet.password(),
sshClientVersion: FakerStatic.lorem.words(2),
connectionUUID: FakerStatic.datatype.uuid(),
};
}
export function fakeAttemptStream(): EventSource {
const es = new EventSource('/stream');
return es;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -1,79 +0,0 @@
<template>
<div class="attemptlist">
<h1>Live Attempts</h1>
<p>
<b-table
responsive="md"
striped
hover
:items="attempts"
:fields="fields"
/>
</p>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { BvTableFieldArray, BvTableFormatterCallback } from 'bootstrap-vue';
import { LoginAttempt } from '@/apiary/apiary';
@Component
export default class AttemptList extends Vue {
@Prop() private items!: LoginAttempt[];
attempts: LoginAttempt[];
fields: BvTableFieldArray = [
{
key: 'date',
sortable: true,
formatter: (value: string): string => {
const d = new Date(value);
// This seems stupid...
return d.toTimeString().split(' ')[0];
},
sortByFormatted: false,
},
{
key: 'username',
},
{
key: 'password',
},
{
key: 'remoteIP',
sortable: true,
},
{
key: 'country',
},
];
constructor() {
super();
this.attempts = [];
}
mounted(): void {
/**
console.log(this);
const at: LoginAttempt[] = [];
for (let i = 0; i < 5; i += 1) {
at.push(fakeAttempt());
}
setInterval(() => {
at.push(fakeAttempt());
}, 1000);
*/
const attemptStream = new EventSource('/api/stream');
attemptStream.addEventListener('message', (ev: MessageEvent<string>) => {
const parsed: LoginAttempt = JSON.parse(ev.data);
this.attempts.unshift(parsed);
});
}
}
</script>

View File

@ -1,145 +0,0 @@
<template>
<div class="home">
<b-container v-if="ready">
<b-row>
<b-col class="total-title">
<h2>Total login attempts</h2>
</b-col>
<b-col class="total-count">
<h2>{{ totalLoginAttempts }}</h2>
</b-col>
</b-row>
<b-row>
<b-col class="total-title">
<h2>Unique passwords</h2>
</b-col>
<b-col class="total-count">
<h2>{{ uniquePasswords }}</h2>
</b-col>
</b-row>
<b-row>
<b-col class="total-title">
<h2>Unique usernames</h2>
</b-col>
<b-col class="total-count">
<h2>{{ uniqueUsernames }}</h2>
</b-col>
</b-row>
<b-row>
<b-col class="total-title">
<h2>Unique IPs</h2>
</b-col>
<b-col class="total-count">
<h2>{{ uniqueIPs }}</h2>
</b-col>
</b-row>
</b-container>
</div>
</template>
<script lang="ts">
import { Component, Vue, Watch } from 'vue-property-decorator';
import axios from 'axios';
type TotalStatsHeaders =
| 'UniquePasswords'
| 'UniqueUsernames'
| 'UniqueIPs'
| 'UniqueCountries'
| 'TotalLoginAttempts';
interface TotalStatsResult {
name: TotalStatsHeaders;
count: number;
}
@Component
export default class Home extends Vue {
totalLoginAttempts: number;
uniquePasswords: number;
uniqueUsernames: number;
uniqueIPs: number;
uniqueCountries: number;
private updaterHandle?: number;
ready = false;
constructor() {
super();
this.totalLoginAttempts = 0;
this.uniquePasswords = 0;
this.uniqueUsernames = 0;
this.uniqueIPs = 0;
this.uniqueCountries = 0;
}
updateStats(): void {
const url = `/api/stats?type=total`;
axios
.get<TotalStatsResult[]>(url)
.then((resp) => {
const totals = resp.data.find((e) => e.name === 'TotalLoginAttempts')
?.count;
if (totals) {
this.totalLoginAttempts = totals;
}
const passwords = resp.data.find((e) => e.name === 'UniquePasswords')
?.count;
if (passwords) {
this.uniquePasswords = passwords;
}
const usernames = resp.data.find((e) => e.name === 'UniqueUsernames')
?.count;
if (usernames) {
this.uniqueUsernames = usernames;
}
const ips = resp.data.find((e) => e.name === 'UniqueIPs')?.count;
if (ips) {
this.uniqueIPs = ips;
}
const countries = resp.data.find((e) => e.name === 'UniqueCountries')
?.count;
if (countries) {
this.uniqueCountries = countries;
}
this.ready = true;
})
.catch((e) => {
console.log(e);
});
}
created(): void {
this.updateStats();
this.updaterHandle = setInterval(() => {
this.updateStats();
}, 5000);
}
beforeDestroy(): void {
clearInterval(this.updaterHandle);
}
}
</script>
<style scoped lang="scss">
.home {
text-align: left;
}
.total-title {
text-align: right;
}
.total-count {
text-align: left;
}
</style>

View File

@ -1,81 +0,0 @@
<template>
<div class="search-result-container">
<h1>Search</h1>
<b-form @submit="onSubmit">
<b-form-input v-model="searchString" placeholder="" />
</b-form>
<b-table
class="search-results-table"
responsive="md"
striped
hover
:items="attempts"
:fields="fields"
/>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { LoginAttempt } from '@/apiary/apiary';
import { BvTableFieldArray, BvTableFormatterCallback } from 'bootstrap-vue';
import Axios from 'axios';
@Component
export default class SearchResult extends Vue {
attempts: LoginAttempt[];
searchString: string;
constructor() {
super();
this.attempts = [];
this.searchString = '';
}
fields: BvTableFieldArray = [
{
key: 'date',
sortable: true,
formatter: (value: string): string => {
const d = new Date(value);
// This seems stupid...
return d.toTimeString().split(' ')[0];
},
sortByFormatted: false,
},
{
key: 'username',
},
{
key: 'password',
},
{
key: 'remoteIP',
sortable: true,
},
{
key: 'country',
},
];
onSubmit(event: Event) {
event.preventDefault();
console.log(this.searchString);
const resp = Axios.get<LoginAttempt[]>(
`/api/query?query=${this.searchString}`,
);
resp.then((r) => {
this.attempts = r.data;
});
}
}
</script>
<style lang="scss" scoped>
.search-results-table {
margin-top: 50px;
}
</style>

View File

@ -1,52 +0,0 @@
<template>
<div class="stats">
<h1>Login Stats</h1>
<b-card no-body>
<b-tabs card>
<b-tab title="Totals">
<b-card-body>
<home></home>
</b-card-body>
</b-tab>
<b-tab title="Usernames">
<b-card-body>
<stats-pie statType="username" />
</b-card-body>
</b-tab>
<b-tab title="Passwords">
<b-card-body>
<stats-pie statType="password"></stats-pie>
</b-card-body>
</b-tab>
<b-tab title="Countries">
<b-card-body>
<stats-pie statType="country"></stats-pie>
</b-card-body>
</b-tab>
<b-tab title="IPs">
<b-card-body>
<stats-pie statType="ip"></stats-pie>
</b-card-body>
</b-tab>
</b-tabs>
</b-card>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import StatsUsername from '@/components/StatsUsername.vue';
import StatsPie from '@/components/StatsPie.vue';
import Home from '@/components/Home.vue';
@Component({
components: {
StatsUsername,
StatsPie,
Home,
},
})
export default class Stats extends Vue {}
</script>
<style lang="scss" scoped>
</style>

View File

@ -1,187 +0,0 @@
<template>
<div class="stats-container" :class="containerClass">
<h2>
{{ title() }}
<b-icon :id="chartSettingsID()" icon="gear-fill"></b-icon>
</h2>
<div class="chart-container">
<canvas :id="chartID()" width="300" height="300"></canvas>
</div>
<b-popover
:target="chartSettingsID()"
triggers="click"
:show.sync="settingsShow"
placement="auto"
container="stats-container"
ref="popover"
@show="onShow"
>
<template #title>
<b-button @click="onClose" class="close" aria-label="Close">
<span class="d-inline-block" aria-hidden="true">&times;</span>
</b-button>
Settings
</template>
<div>
<b-form-group
label="Limit"
label-for="limitinput"
label-cols="3"
class="mb-1"
description="Limit number of items"
invalid-feedback="This field is required"
>
<b-form-input
ref="limitinput"
id="limitinput"
v-model="limitState"
type="number"
size="sm"
></b-form-input>
</b-form-group>
<b-button @click="onClose" size="sm" variant="danger">Cancel</b-button>
<b-button @click="onOk" size="sm" variant="primary">Ok</b-button>
</div>
</b-popover>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import { StatResult } from '@/apiary/apiary';
import axios from 'axios';
import { Chart, ArcElement, Legend, DoughnutController, Title } from 'chart.js';
import randomColor from 'randomcolor';
export type StatType = 'username' | 'password' | 'ip' | 'country' | 'total';
Chart.register(ArcElement, Legend, DoughnutController, Title);
@Component
export default class StatsPie extends Vue {
@Prop() private statType!: StatType;
limit: number;
limitState: number;
settingsShow = false;
stats: StatResult[];
chart?: Chart;
constructor() {
super();
this.stats = [];
this.limit = 10;
this.limitState = 10;
}
title(): string {
switch (this.statType) {
case 'password':
return `Top ${this.limit} Passwords`;
case 'username':
return `Top ${this.limit} Usernames`;
case 'ip':
return `Top ${this.limit} IPs`;
case 'country':
return `Top ${this.limit} Countries`;
case 'total':
return 'Totals';
// Why doesn't eslint know that this switch is exhaustive?
default:
return 'Top 10 Passwords';
}
}
containerClass(): string {
return `stats-container-${this.statType}`;
}
chartID(): string {
return `chart-${this.statType}`;
}
chartSettingsID(): string {
return `chartsettings-${this.statType}`;
}
settingsID(): string {
return `settings-${this.statType}`;
}
onClose(): void {
this.settingsShow = false;
}
onOk(): void {
if (this.limitState) {
this.limit = this.limitState;
this.settingsShow = false;
}
}
onShow(): void {
// This is called just before the popover is shown
// Reset our popover form variables
this.limitState = this.limit;
}
@Watch('limit')
limitChanged(value: number, oldValue: number): void {
this.fetchData();
}
fetchData(): void {
const url = `/api/stats?type=${this.statType}&limit=${this.limit}`;
axios.get<StatResult[]>(url).then((resp) => {
this.stats = resp.data;
this.renderPie();
});
}
mounted(): void {
this.fetchData();
}
renderPie(): void {
const elem = document.getElementById(this.chartID()) as HTMLCanvasElement;
const ctx = elem.getContext('2d') as CanvasRenderingContext2D;
const sortedStats = this.stats.sort();
const values = sortedStats.map((s) => s.count);
const headers = sortedStats.map((s) => s.name);
const colors = sortedStats.map(() => randomColor());
if (this.chart) {
this.chart.destroy();
}
this.chart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: headers,
datasets: [
{
data: values,
backgroundColor: colors,
},
],
},
});
}
}
</script>
<style lang="scss" scoped>
canvas {
width: 70%;
height: auto;
max-height: 60vmin;
}
.stats-container {
align-content: center;
}
</style>

View File

@ -1,67 +0,0 @@
<template>
<div class="stats-container">
<h2>Top 10 usernames</h2>
<div class="chart-container">
<canvas id="chart" widht="400" height="400"></canvas>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { StatResult } from '@/apiary/apiary';
import axios from 'axios';
import Chart from 'chart.js/auto';
import randomColor from 'randomcolor';
@Component
export default class StatsUsername extends Vue {
@Prop() private msg!: string;
stats: StatResult[];
constructor() {
super();
this.stats = [];
}
mounted(): void {
axios
.get<StatResult[]>('/api/stats?type=username&limit=10')
.then((resp) => {
this.stats = resp.data;
this.renderPie();
});
}
renderPie(): void {
const elem = document.getElementById('chart') as HTMLCanvasElement;
const ctx = elem.getContext('2d') as CanvasRenderingContext2D;
const sortedStats = this.stats.sort();
const values = sortedStats.map((s) => s.count);
const headers = sortedStats.map((s) => s.name);
const colors = sortedStats.map(() => randomColor());
const chart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: headers,
options: {},
datasets: [
{
data: values,
backgroundColor: colors,
},
],
},
});
}
}
</script>
<style lang="scss" scoped>
.chart-container {
max-width: 500px;
max-height: 500px;
}
</style>

View File

@ -1,58 +0,0 @@
import Vue from 'vue';
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue';
import VueRouter, { RouteConfig } from 'vue-router';
import AttemptList from '@/components/AttemptList.vue';
import Home from '@/components/Home.vue';
import Stats from '@/components/Stats.vue';
import SearchResult from '@/components/SearchResult.vue';
import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap-vue/dist/bootstrap-vue.css';
import '@fontsource/rubik';
import '@fontsource/secular-one';
import App from './App.vue';
Vue.config.productionTip = false;
// Make BootstrapVue available throughout your project
Vue.use(BootstrapVue);
// Optionally install the BootstrapVue icon components plugin
Vue.use(IconsPlugin);
Vue.use(VueRouter);
const routes: Array<RouteConfig> = [
{
path: '/',
name: 'Home',
component: Home,
alias: '/home',
},
{
path: '/attempts',
name: 'Attempt List',
component: AttemptList,
props: {
// items: testAttempts,
},
},
{
path: '/stats',
name: 'Stats',
component: Stats,
},
{
path: '/search',
name: 'Search',
component: SearchResult,
}
];
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes,
});
new Vue({
router,
render: (h) => h(App),
}).$mount('#app');

View File

@ -1,13 +0,0 @@
import Vue, { VNode } from 'vue';
declare global {
namespace JSX {
// tslint:disable no-empty-interface
interface Element extends VNode {}
// tslint:disable no-empty-interface
interface ElementClass extends Vue {}
interface IntrinsicElements {
[elem: string]: any
}
}
}

View File

@ -1,5 +0,0 @@
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}

View File

@ -1,40 +0,0 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}

File diff suppressed because it is too large Load Diff

View File

@ -17,13 +17,11 @@ func (s *Server) LoggingMiddleware(next http.Handler) http.Handler {
reqID := middleware.GetReqID(r.Context()) reqID := middleware.GetReqID(r.Context())
defer func() { defer func() {
/*s.AccessLogger.Debugw(r.Method, // If AccessLogIgnoreMetrics is true, do not log successful requests to metrics endpoint
"path", r.URL.Path, if s.cfg.AccessLogIgnoreMetrics && r.URL.Path == "/metrics" && ww.Status() == http.StatusOK {
"request-headers", r.Header, return
"response-headers", ww.Header(), }
) s.AccessLogger.Info(r.Method,
*/
s.AccessLogger.Infow(r.Method,
"path", r.URL.Path, "path", r.URL.Path,
"status", ww.Status(), "status", ww.Status(),
"written", ww.BytesWritten(), "written", ww.BytesWritten(),

View File

@ -6,6 +6,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"log/slog"
"net/http" "net/http"
"strconv" "strconv"
"sync" "sync"
@ -20,7 +21,6 @@ import (
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
"go.uber.org/zap"
"golang.org/x/crypto/acme/autocert" "golang.org/x/crypto/acme/autocert"
) )
@ -35,8 +35,8 @@ type Server struct {
honeypotServer *ssh.HoneypotServer honeypotServer *ssh.HoneypotServer
store store.LoginAttemptStore store store.LoginAttemptStore
ServerLogger *zap.SugaredLogger ServerLogger *slog.Logger
AccessLogger *zap.SugaredLogger AccessLogger *slog.Logger
attemptListenersLock sync.RWMutex attemptListenersLock sync.RWMutex
attemptListeners map[string]chan models.LoginAttempt attemptListeners map[string]chan models.LoginAttempt
@ -44,9 +44,10 @@ type Server struct {
} }
func NewServer(cfg config.FrontendConfig, hs *ssh.HoneypotServer, store store.LoginAttemptStore) *Server { func NewServer(cfg config.FrontendConfig, hs *ssh.HoneypotServer, store store.LoginAttemptStore) *Server {
discardLogger := slog.New(slog.NewTextHandler(io.Discard, nil))
s := &Server{ s := &Server{
ServerLogger: zap.NewNop().Sugar(), ServerLogger: discardLogger,
AccessLogger: zap.NewNop().Sugar(), AccessLogger: discardLogger,
store: store, store: store,
cfg: cfg, cfg: cfg,
} }
@ -67,7 +68,7 @@ func NewServer(cfg config.FrontendConfig, hs *ssh.HoneypotServer, store store.Lo
s.RegisterOnShutdown(func() { s.RegisterOnShutdown(func() {
timeoutCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second) timeoutCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel() defer cancel()
s.httpRedirectServer.Shutdown(timeoutCtx) s.httpRedirectServer.Shutdown(timeoutCtx) // nolint: errcheck
}) })
s.Addr = ":443" s.Addr = ":443"
@ -85,10 +86,12 @@ func NewServer(cfg config.FrontendConfig, hs *ssh.HoneypotServer, store store.Lo
r.Use(middleware.RealIP) r.Use(middleware.RealIP)
r.Use(middleware.RequestID) r.Use(middleware.RequestID)
r.Use(s.LoggingMiddleware) r.Use(s.LoggingMiddleware)
r.Use(NewMetricsMiddleware())
r.Use(middleware.SetHeader("Server", fmt.Sprintf("apiary/%s", apiary.FullVersion()))) r.Use(middleware.SetHeader("Server", fmt.Sprintf("apiary/%s", apiary.FullVersion())))
r.Handle("/metrics", promhttp.Handler()) if cfg.MetricsEnable {
r.Use(NewMetricsMiddleware())
r.Handle("/metrics", promhttp.Handler())
}
r.Route("/", func(r chi.Router) { r.Route("/", func(r chi.Router) {
r.Get("/*", s.IndexHandler("web/vue-frontend/dist")) r.Get("/*", s.IndexHandler("web/vue-frontend/dist"))
r.Get("/stream", s.HandlerAttemptStream) r.Get("/stream", s.HandlerAttemptStream)
@ -125,7 +128,7 @@ func (s *Server) StartServe() error {
s.ServerLogger.Debug("Starting HTTP redirect server") s.ServerLogger.Debug("Starting HTTP redirect server")
go func() { go func() {
if err := s.httpRedirectServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { if err := s.httpRedirectServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
s.ServerLogger.Warnw("HTTP redirect server returned error", "error", err) s.ServerLogger.Warn("HTTP redirect server returned error", "error", err)
} }
}() }()
} }
@ -151,6 +154,7 @@ func (s *Server) closeAttemptListener(id string) {
close(ch) close(ch)
delete(s.attemptListeners, id) delete(s.attemptListeners, id)
} }
func (s *Server) HandlerAttemptStream(w http.ResponseWriter, r *http.Request) { func (s *Server) HandlerAttemptStream(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Cache-Control", "no-cache")
@ -173,7 +177,7 @@ func (s *Server) HandlerAttemptStream(w http.ResponseWriter, r *http.Request) {
} }
_, err = io.WriteString(w, fmt.Sprintf("data: %s\n\n", string(data))) _, err = io.WriteString(w, fmt.Sprintf("data: %s\n\n", string(data)))
if err != nil { if err != nil {
s.ServerLogger.Warnw("Error writing event", "error", err) s.ServerLogger.Warn("Error writing event", "error", err)
} }
flusher.Flush() flusher.Flush()
ticker.Reset(streamKeepAliveDuration) ticker.Reset(streamKeepAliveDuration)
@ -183,7 +187,7 @@ func (s *Server) HandlerAttemptStream(w http.ResponseWriter, r *http.Request) {
return return
case <-ticker.C: case <-ticker.C:
if _, err := io.WriteString(w, ": keep-alive\n\n"); err != nil { if _, err := io.WriteString(w, ": keep-alive\n\n"); err != nil {
s.ServerLogger.Warnw("Error writing event", "error", err) s.ServerLogger.Warn("Error writing event", "error", err)
} }
flusher.Flush() flusher.Flush()
} }
@ -206,16 +210,17 @@ func (s *Server) HandlerStats(w http.ResponseWriter, r *http.Request) {
stats, err := s.store.Stats(statType, limit) stats, err := s.store.Stats(statType, limit)
if err != nil { if err != nil {
s.ServerLogger.Warnw("Error fetching stats", "error", err) s.ServerLogger.Warn("Error fetching stats", "error", err)
s.WriteAPIError(w, r, http.StatusInternalServerError, "Error fetching stats") s.WriteAPIError(w, r, http.StatusInternalServerError, "Error fetching stats")
return return
} }
encoder := json.NewEncoder(w) encoder := json.NewEncoder(w)
if err := encoder.Encode(stats); err != nil { if err := encoder.Encode(stats); err != nil {
s.ServerLogger.Debugf("Error encoding or writing response", "remote_ip", r.RemoteAddr, "error", err) s.ServerLogger.Debug("Error encoding or writing response", "remote_ip", r.RemoteAddr, "error", err)
} }
} }
func (s *Server) HandlerQuery(w http.ResponseWriter, r *http.Request) { func (s *Server) HandlerQuery(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
queryType := r.URL.Query().Get("type") queryType := r.URL.Query().Get("type")
@ -239,13 +244,13 @@ func (s *Server) HandlerQuery(w http.ResponseWriter, r *http.Request) {
userResults, err := s.store.Query(uq) userResults, err := s.store.Query(uq)
if err != nil { if err != nil {
s.WriteAPIError(w, r, http.StatusInternalServerError, "Unable to perform query") s.WriteAPIError(w, r, http.StatusInternalServerError, "Unable to perform query")
s.ServerLogger.Warnw("Error performing query", "error", err) s.ServerLogger.Warn("Error performing query", "error", err)
return return
} }
passwordResults, err := s.store.Query(pq) passwordResults, err := s.store.Query(pq)
if err != nil { if err != nil {
s.WriteAPIError(w, r, http.StatusInternalServerError, "Unable to perform query") s.WriteAPIError(w, r, http.StatusInternalServerError, "Unable to perform query")
s.ServerLogger.Warnw("Error performing query", "error", err) s.ServerLogger.Warn("Error performing query", "error", err)
return return
} }
@ -261,7 +266,7 @@ func (s *Server) HandlerQuery(w http.ResponseWriter, r *http.Request) {
queryResults, err := s.store.Query(aq) queryResults, err := s.store.Query(aq)
if err != nil { if err != nil {
s.WriteAPIError(w, r, http.StatusInternalServerError, "Unable to perform query") s.WriteAPIError(w, r, http.StatusInternalServerError, "Unable to perform query")
s.ServerLogger.Warnw("Error performing query", "error", err) s.ServerLogger.Warn("Error performing query", "error", err)
return return
} }
@ -270,7 +275,7 @@ func (s *Server) HandlerQuery(w http.ResponseWriter, r *http.Request) {
encoder := json.NewEncoder(w) encoder := json.NewEncoder(w)
if err := encoder.Encode(&results); err != nil { if err := encoder.Encode(&results); err != nil {
s.ServerLogger.Warnw("Error writing query results", "error", err) s.ServerLogger.Warn("Error writing query results", "error", err)
} }
} }
@ -280,7 +285,7 @@ func (s *Server) HandlerHealth(w http.ResponseWriter, r *http.Request) {
s.WriteAPIError(w, r, http.StatusInternalServerError, fmt.Sprintf("Health error: %s", err)) s.WriteAPIError(w, r, http.StatusInternalServerError, fmt.Sprintf("Health error: %s", err))
return return
} }
w.Write([]byte(`{}`)) w.Write([]byte(`{}`)) // nolint: errcheck
} }
type APIErrorResponse struct { type APIErrorResponse struct {
@ -292,6 +297,6 @@ func (s *Server) WriteAPIError(w http.ResponseWriter, r *http.Request, status in
apiErr := APIErrorResponse{Error: message} apiErr := APIErrorResponse{Error: message}
w.WriteHeader(status) w.WriteHeader(status)
if err := encoder.Encode(&apiErr); err != nil { if err := encoder.Encode(&apiErr); err != nil {
s.ServerLogger.Debugf("Error encoding or writing error response", "remote_ip", r.RemoteAddr, "error", err) s.ServerLogger.Debug("Error encoding or writing error response", "remote_ip", r.RemoteAddr, "error", err)
} }
} }