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
**/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
RUN mkdir -p /app/frontend
COPY web/frontend/yarn.lock /app/frontend/yarn.lock
COPY web/frontend/package.json /app/frontend/package.json
WORKDIR /app/frontend
RUN yarn install
COPY web/frontend /app/frontend
WORKDIR /app/frontend
RUN GENERATE_SOURCEMAP=false yarn build
FROM node:alpine as frontend-builder
WORKDIR /app
COPY frontend/package.json /app
COPY frontend/package-lock.json /app
RUN npm install
COPY frontend .
RUN npm run 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
WORKDIR /app
@ -14,16 +18,15 @@ COPY go.mod /app/go.mod
COPY go.sum /app/go.sum
RUN go mod download
COPY . /app
RUN rm -rf /app/server/frontend
COPY --from=frontend-builder /app/frontend/dist /app/web/frontend/dist
RUN CGO_ENABLED=0 INSTALL_PREFIX=/app make install
COPY --from=geoip-fetcher /app/geoacumen-country/Geoacumen-Country.mmdb honeypot/ssh
COPY --from=frontend-builder /app/dist /app/web/frontend/dist
RUN CGO_ENABLED=0 go build -tags embed cmd/apiary/apiary.go
FROM alpine:latest
RUN apk add --no-cache curl
WORKDIR /app
COPY --from=builder /app/etc/apiary/apiary.toml /app/apiary.toml
COPY --from=builder /app/bin/apiary /app/apiary
COPY --from=builder /app/apiary.toml /app/apiary.toml
COPY --from=builder /app/apiary /app/apiary
EXPOSE 8080
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"]

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.
## TODO
## Build using nix
* Fix janky ass Makefile
```console
$ nix build .#
## Requirements
* `go >= 1.16`
Requires a postgres database if you want data to persist through server restart.
## Build
```text
# make frontend
# make build
$ ./result/bin/apiary --version
apiary version v0.2.1-d86bc2be079c6b18a9f941752ebad45d925366d1 (go1.23.6)
```
## Build without nix
See [Dockerfile](./Dockerfile) for how to build frontend and backend. Or use Dockerfile
and copy binary from image.
## Install
```text
# Build and install
INSTALL_PREFIX=/ sudo make install
```console
Build or download binary
$ cp ./apiary /usr/local/bin/apiary
# Edit config file
vim /etc/apiary/apiary.toml
Edit config file, should use postgres if you want persistent storage.
See example config for the different options.
$ vim /etc/apiary/apiary.toml
# Run
/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 web server
$ /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 web server
```

View File

@ -21,7 +21,7 @@ HostKeyPath = ""
# Log level for SSH Honeypot
# Must be either "DEBUG", "INFO", "WARN", "ERROR", "FATAL", "NONE"
# Default: "INFO"
LogLevel = "INFO"
LogLevel = "DEBUG"
# Address and port to listen to
# Default: ":2222"
ListenAddr = ":2222"
@ -35,9 +35,15 @@ ThrottleSpeed = 10240.0
# Must be either "DEBUG", "INFO", "WARN", "ERROR", "FATAL", "NONE"
# Default: "INFO"
LogLevel = "INFO"
# Enable metrics endpoint
# Default: false
MetricsEnable = false
# Enable access logging
# Default: true
AccessLogEnable = true
# Disable logging of requests to metrics endpoint
# Default: false
AccessLogIgnoreMetrics = false
# Address and port to listen to
# Default: ":8080"
ListenAddr = ":8080"
@ -60,32 +66,3 @@ CacheDir = "/var/apiary/certs"
# Default: 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 (
"context"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
@ -11,29 +12,19 @@ import (
"git.t-juice.club/torjus/apiary"
"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/store"
"git.t-juice.club/torjus/apiary/web"
"github.com/coreos/go-systemd/daemon"
sshlib "github.com/gliderlabs/ssh"
"github.com/urfave/cli/v2"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"github.com/urfave/cli/v3"
"golang.org/x/crypto/acme/autocert"
)
func main() {
app := &cli.App{
app := &cli.Command{
Name: "apiary",
Version: apiary.FullVersion(),
Authors: []*cli.Author{
{
Name: "Torjus Håkestad",
Email: "torjus@usit.uio.no",
},
},
Commands: []*cli.Command{
{
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)
os.Exit(1)
}
}
func ActionServe(c *cli.Context) error {
func ActionServe(ctx context.Context, cmd *cli.Command) error {
cfg, err := getConfig()
if err != nil {
return err
@ -57,17 +48,17 @@ func ActionServe(c *cli.Context) error {
// Setup logging
loggers := setupLoggers(cfg)
loggers.rootLogger.Infow("Starting apiary", "version", apiary.FullVersion())
loggers.rootLogger.Info("Starting apiary.", "version", apiary.FullVersion())
// Setup store
var s store.LoginAttemptStore
switch cfg.Store.Type {
case "MEMORY", "memory":
loggers.rootLogger.Infow("Initialized store", "store_type", "memory")
loggers.rootLogger.Info("Initialized store.", "store_type", "memory")
s = &store.MemoryStore{}
case "POSTGRES", "postgres":
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)
if err != nil {
return err
@ -75,13 +66,13 @@ func ActionServe(c *cli.Context) error {
if err := pgStore.InitDB(); err != nil {
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 {
loggers.rootLogger.Debugw("Initializing store", "store_type", "cache-postgres")
loggers.rootLogger.Debug("Initializing store.", "store_type", "cache-postgres")
startTime := time.Now()
cachingStore := store.NewCachingStore(pgStore)
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 {
s = pgStore
}
@ -93,7 +84,7 @@ func ActionServe(c *cli.Context) error {
interruptChan := make(chan os.Signal, 1)
signal.Notify(interruptChan, os.Interrupt)
rootCtx, rootCancel := context.WithCancel(c.Context)
rootCtx, rootCancel := context.WithCancel(ctx)
defer rootCancel()
serversCtx, serversCancel := context.WithCancel(rootCtx)
defer serversCancel()
@ -126,78 +117,27 @@ func ActionServe(c *cli.Context) error {
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
go func() {
<-interruptChan
loggers.rootLogger.Info("Interrupt received, shutting down")
loggers.rootLogger.Info("Interrupt received, shutting down.")
serversCancel()
}()
// Start ssh server
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 {
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
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 {
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
}
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
}
loggers.rootLogger.Debug("Sent READY=1 to NOTIFY_SOCKET.")
@ -225,11 +165,11 @@ func ActionServe(c *cli.Context) error {
// Setup timer
timeout, err := daemon.SdWatchdogEnabled(false)
if err != nil {
loggers.rootLogger.Warnw("Unable to get watchdog timeout.", "error", err)
loggers.rootLogger.Warn("Unable to get watchdog timeout.", "error", err)
return
}
if timeout == 0 {
loggers.rootLogger.Infow("Systemd watchdog not enabled.")
loggers.rootLogger.Info("Systemd watchdog not enabled.")
return
}
@ -241,13 +181,15 @@ func ActionServe(c *cli.Context) error {
select {
case <-ticker.C:
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
}
// 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():
loggers.rootLogger.Debugw("Notify context cancelled.")
loggers.rootLogger.Debug("Notify context cancelled.")
return
}
}
@ -259,80 +201,61 @@ func ActionServe(c *cli.Context) error {
// Stop SSH server
sshShutdownCtx, sshShutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer sshShutdownCancel()
loggers.rootLogger.Info("SSH server shutdown started")
loggers.rootLogger.Info("SSH server shutdown started.")
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
webShutdownCtx, webShutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer webShutdownCancel()
loggers.rootLogger.Info("Web server shutdown started")
loggers.rootLogger.Info("Web server shutdown started.")
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()
}()
<-rootCtx.Done()
return nil
}
type loggerCollection struct {
rootLogger *zap.SugaredLogger
honeypotLogger *zap.SugaredLogger
webAccessLogger *zap.SugaredLogger
webServerLogger *zap.SugaredLogger
portsLogger *zap.SugaredLogger
smtpLogger *zap.SugaredLogger
rootLogger *slog.Logger
honeypotLogger *slog.Logger
webAccessLogger *slog.Logger
webServerLogger *slog.Logger
}
func setupLoggers(cfg config.Config) *loggerCollection {
logEncoderCfg := zap.NewProductionEncoderConfig()
logEncoderCfg.EncodeCaller = func(caller zapcore.EntryCaller, enc zapcore.PrimitiveArrayEncoder) {}
var level zap.AtomicLevel
opts := &slog.HandlerOptions{}
switch strings.ToUpper(cfg.Honeypot.LogLevel) {
case "INFO":
level = zap.NewAtomicLevelAt(zap.InfoLevel)
opts.Level = slog.LevelInfo
case "DEBUG":
level = zap.NewAtomicLevelAt(zap.DebugLevel)
opts.Level = slog.LevelDebug
opts.AddSource = true
case "WARN", "WARNING":
level = zap.NewAtomicLevelAt(zap.WarnLevel)
opts.Level = slog.LevelWarn
case "ERR", "ERROR":
level = zap.NewAtomicLevelAt(zap.WarnLevel)
opts.Level = slog.LevelError
default:
level = zap.NewAtomicLevelAt(zap.InfoLevel)
}
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)
opts.Level = slog.LevelInfo
}
handler := slog.NewTextHandler(os.Stdout, opts)
rootLogger := slog.New(handler)
return &loggerCollection{
rootLogger: rootLogger.Named("APP").Sugar(),
honeypotLogger: rootLogger.Named("HON").Sugar(),
webAccessLogger: rootLogger.Named("ACC").Sugar(),
webServerLogger: rootLogger.Named("WEB").Sugar(),
portsLogger: rootLogger.Named("PRT").Sugar(),
smtpLogger: rootLogger.Named("SMT").Sugar(),
rootLogger: rootLogger.With("module", "application"),
honeypotLogger: rootLogger.With("module", "honeypot"),
webAccessLogger: rootLogger.With("module", "web-access-log"),
webServerLogger: rootLogger.With("module", "web-server"),
}
}
func getConfig() (config.Config, error) {

View File

@ -6,15 +6,13 @@ import (
"os"
"strings"
"github.com/pelletier/go-toml"
"github.com/pelletier/go-toml/v2"
)
type Config struct {
Store StoreConfig `toml:"Store"`
Honeypot HoneypotConfig `toml:"Honeypot"`
Frontend FrontendConfig `toml:"Frontend"`
Ports PortsConfig `toml:"Ports"`
SMTP SMTPConfig `toml:"SMTP"`
}
type StoreConfig struct {
Type string `toml:"Type"`
@ -34,10 +32,12 @@ type HoneypotConfig struct {
}
type FrontendConfig struct {
ListenAddr string `toml:"ListenAddr"`
LogLevel string `toml:"LogLevel"`
AccessLogEnable bool `toml:"AccessLogEnable"`
Autocert FrontendAutocertConfig `toml:"Autocert"`
ListenAddr string `toml:"ListenAddr"`
LogLevel string `toml:"LogLevel"`
MetricsEnable bool `toml:"MetricsEnable"`
AccessLogEnable bool `toml:"AccessLogEnable"`
AccessLogIgnoreMetrics bool `toml:"AccessLogIgnoreMetrics"`
Autocert FrontendAutocertConfig `toml:"Autocert"`
}
type FrontendAutocertConfig struct {
@ -48,19 +48,6 @@ type FrontendAutocertConfig struct {
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) {
var c Config
@ -70,7 +57,6 @@ func FromReader(r io.Reader) (Config, error) {
c.Frontend.ListenAddr = ":8080"
c.Frontend.LogLevel = "INFO"
c.Frontend.AccessLogEnable = true
c.SMTP.Addr = ":25"
// Read from config
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)
}
//c.readEnv()
// c.readEnv()
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
go 1.16
go 1.24.0
toolchain go1.24.1
require (
github.com/cespare/xxhash/v2 v2.1.2 // indirect
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/gliderlabs/ssh v0.3.3
github.com/go-chi/chi/v5 v5.0.4
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/uuid v1.3.0
github.com/jackc/pgx/v4 v4.13.0
github.com/oschwald/maxminddb-golang v1.8.0
github.com/pelletier/go-toml v1.9.4
github.com/prometheus/client_golang v1.11.0
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/urfave/cli/v2 v2.3.0
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.7.0 // indirect
go.uber.org/zap v1.19.1
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6 // indirect
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
google.golang.org/protobuf v1.27.1 // indirect
github.com/gliderlabs/ssh v0.3.8
github.com/go-chi/chi/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.7.2
github.com/oschwald/maxminddb-golang v1.13.1
github.com/pelletier/go-toml/v2 v2.2.3
github.com/prometheus/client_golang v1.21.1
github.com/urfave/cli/v3 v3.0.0-beta1
golang.org/x/crypto v0.36.0
)
require (
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/icholy/gomajor v0.14.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // 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/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/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.1.1/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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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/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/go.mod h1:LmEmu6L/8jetyj1oewewFb7bZCNRwE7wLCUNzDLaLVA=
github.com/gliderlabs/ssh v0.3.3 h1:mBQ8NiOgDkINJrZtoizkC3nDNYgSaWtxyem6S2XHBtA=
github.com/gliderlabs/ssh v0.3.3/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914=
github.com/go-chi/chi/v5 v5.0.4 h1:5e494iHzsYBiyXQAHHuI4tyJS9M3V84OuX3ufIIGHFo=
github.com/go-chi/chi/v5 v5.0.4/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
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/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786 h1:rcv+Ippz6RAtvaGgKxc+8FQIpxHgsF+HBzPyYL2cyVU=
github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
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.3.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/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/icholy/gomajor v0.14.0 h1:qr0eGSMyLcZa7lmSuiplH3kr6j4oH6ET8nkdZqpDcks=
github.com/icholy/gomajor v0.14.0/go.mod h1:oJuk5dppdmnvUQRKsUNelWZ7KmYpvYKVupt/OQ0y9UI=
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/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1 h1:7PQ/4gLoqnl87ZxL7xjO0DR5gYuviDCZxQJsUlFW1eI=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
github.com/jackc/pgtype v1.8.1 h1:9k0IXtdJXHJbyAWQgbWr1lU+MEhPXZz6RIXxfR5oxXs=
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/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
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/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM=
github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg=
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.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/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4=
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=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjcw9Zg=
github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7 h1:FemxDzfMUcK2f3YY4H+05K9CDzbSVr2+q/JKN45pey0=
golang.org/x/telemetry v0.0.0-20240522233618-39ace7a40ae7/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
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.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
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=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
golang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I=
golang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
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-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

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

View File

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

View File

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

View File

@ -9,6 +9,8 @@ import (
"git.t-juice.club/torjus/apiary/models"
)
var _ LoginAttemptStore = &MemoryStore{}
type MemoryStore struct {
lock sync.RWMutex
attempts []models.LoginAttempt
@ -32,8 +34,17 @@ func (ms *MemoryStore) AddAttempt(l *models.LoginAttempt) error {
return nil
}
func (ms *MemoryStore) All() ([]models.LoginAttempt, error) {
return ms.attempts, nil
func (ms *MemoryStore) All() (<-chan models.LoginAttempt, error) {
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) {

View File

@ -10,6 +10,7 @@ func TestMemoryStore(t *testing.T) {
s := &store.MemoryStore{}
testLoginAttemptStore(s, t)
}
func TestMemoryStoreWithCache(t *testing.T) {
backend := &store.MemoryStore{}
s := store.NewCachingStore(backend)
@ -22,3 +23,8 @@ func BenchmarkMemoryStore(b *testing.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"
)
var _ LoginAttemptStore = &MetricsCollectingStore{}
const tickDuration = 5 * time.Second
type MetricsCollectingStore struct {
@ -75,7 +77,7 @@ func NewMetricsCollectingStore(ctx context.Context, store LoginAttemptStore) *Me
case <-ctx.Done():
return
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
}
func (s *MetricsCollectingStore) All() ([]models.LoginAttempt, error) {
func (s *MetricsCollectingStore) All() (<-chan models.LoginAttempt, error) {
return s.store.All()
}
@ -114,7 +116,6 @@ func (s *MetricsCollectingStore) Stats(statType LoginStats, limit int) ([]StatsR
}
}
return stats, err
}
func (s *MetricsCollectingStore) Query(query AttemptQuery) ([]models.LoginAttempt, error) {

View File

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

View File

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

View File

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

View File

@ -25,8 +25,12 @@ func testLoginAttemptStore(s store.LoginAttemptStore, t *testing.T) {
if err != nil {
t.Fatalf("Error getting all attempts: %s", err)
}
if len(all) != len(testAttempts) {
t.Errorf("All returned wrong amount. Got %d want %d", len(all), len(testAttempts))
var count int
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)
if err != nil {
@ -229,7 +233,11 @@ func benchmarkLoginAttemptStore(setupFunc func() store.LoginAttemptStore, b *tes
if err != nil {
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
}
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 (
Version = "v0.1.27"
Version = "v0.2.4"
Build string
)

View File

@ -1,3 +1,4 @@
//go:build !embed
// +build !embed
package web
@ -21,7 +22,7 @@ func (s *Server) IndexHandler(frontendDir string) http.HandlerFunc {
fname := filepath.Join(frontendDir, r.URL.Path)
_, err := os.Stat(fname)
if os.IsNotExist(err) {
serveIndex(frontendDir).ServeHTTP(w, r)
s.serveIndex(frontendDir).ServeHTTP(w, r)
return
}
fs.ServeHTTP(w, r)
@ -29,7 +30,7 @@ func (s *Server) IndexHandler(frontendDir string) http.HandlerFunc {
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) {
fname := filepath.Join(frontendDir, "index.html")
f, err := os.Open(fname)
@ -38,7 +39,9 @@ func serveIndex(frontendDir string) http.HandlerFunc {
}
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
}

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())
defer func() {
/*s.AccessLogger.Debugw(r.Method,
"path", r.URL.Path,
"request-headers", r.Header,
"response-headers", ww.Header(),
)
*/
s.AccessLogger.Infow(r.Method,
// If AccessLogIgnoreMetrics is true, do not log successful requests to metrics endpoint
if s.cfg.AccessLogIgnoreMetrics && r.URL.Path == "/metrics" && ww.Status() == http.StatusOK {
return
}
s.AccessLogger.Info(r.Method,
"path", r.URL.Path,
"status", ww.Status(),
"written", ww.BytesWritten(),

View File

@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"strconv"
"sync"
@ -20,7 +21,6 @@ import (
"github.com/go-chi/chi/v5/middleware"
"github.com/google/uuid"
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.uber.org/zap"
"golang.org/x/crypto/acme/autocert"
)
@ -35,8 +35,8 @@ type Server struct {
honeypotServer *ssh.HoneypotServer
store store.LoginAttemptStore
ServerLogger *zap.SugaredLogger
AccessLogger *zap.SugaredLogger
ServerLogger *slog.Logger
AccessLogger *slog.Logger
attemptListenersLock sync.RWMutex
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 {
discardLogger := slog.New(slog.NewTextHandler(io.Discard, nil))
s := &Server{
ServerLogger: zap.NewNop().Sugar(),
AccessLogger: zap.NewNop().Sugar(),
ServerLogger: discardLogger,
AccessLogger: discardLogger,
store: store,
cfg: cfg,
}
@ -67,7 +68,7 @@ func NewServer(cfg config.FrontendConfig, hs *ssh.HoneypotServer, store store.Lo
s.RegisterOnShutdown(func() {
timeoutCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
s.httpRedirectServer.Shutdown(timeoutCtx)
s.httpRedirectServer.Shutdown(timeoutCtx) // nolint: errcheck
})
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.RequestID)
r.Use(s.LoggingMiddleware)
r.Use(NewMetricsMiddleware())
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.Get("/*", s.IndexHandler("web/vue-frontend/dist"))
r.Get("/stream", s.HandlerAttemptStream)
@ -125,7 +128,7 @@ func (s *Server) StartServe() error {
s.ServerLogger.Debug("Starting HTTP redirect server")
go func() {
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)
delete(s.attemptListeners, id)
}
func (s *Server) HandlerAttemptStream(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
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)))
if err != nil {
s.ServerLogger.Warnw("Error writing event", "error", err)
s.ServerLogger.Warn("Error writing event", "error", err)
}
flusher.Flush()
ticker.Reset(streamKeepAliveDuration)
@ -183,7 +187,7 @@ func (s *Server) HandlerAttemptStream(w http.ResponseWriter, r *http.Request) {
return
case <-ticker.C:
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()
}
@ -206,16 +210,17 @@ func (s *Server) HandlerStats(w http.ResponseWriter, r *http.Request) {
stats, err := s.store.Stats(statType, limit)
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")
return
}
encoder := json.NewEncoder(w)
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) {
w.Header().Add("Content-Type", "application/json")
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)
if err != nil {
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
}
passwordResults, err := s.store.Query(pq)
if err != nil {
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
}
@ -261,7 +266,7 @@ func (s *Server) HandlerQuery(w http.ResponseWriter, r *http.Request) {
queryResults, err := s.store.Query(aq)
if err != nil {
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
}
@ -270,7 +275,7 @@ func (s *Server) HandlerQuery(w http.ResponseWriter, r *http.Request) {
encoder := json.NewEncoder(w)
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))
return
}
w.Write([]byte(`{}`))
w.Write([]byte(`{}`)) // nolint: errcheck
}
type APIErrorResponse struct {
@ -292,6 +297,6 @@ func (s *Server) WriteAPIError(w http.ResponseWriter, r *http.Request, status in
apiErr := APIErrorResponse{Error: message}
w.WriteHeader(status)
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)
}
}