First commit
All checks were successful
goreleaser / goreleaser (push) Successful in 56s

This commit is contained in:
adrien 2023-07-01 16:17:20 +02:00
commit 1d7c7fba86
Signed by: adrien
GPG Key ID: 4F17BEA67707AC21
10 changed files with 855 additions and 0 deletions

View File

@ -0,0 +1,33 @@
name: goreleaser
on:
push:
tags:
- '*'
permissions:
contents: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Install syft
run: curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Golang
uses: actions/setup-go@v3
with:
go-version: '1.20.5'
- name: Run GoReleaser
uses: https://github.com/goreleaser/goreleaser-action@v4
with:
distribution: goreleaser
version: latest
args: release --clean
env:
GORELEASER_FORCE_TOKEN: 'gitea'
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.idea
ding
!ding/
dist/

29
.goreleaser.yaml Normal file
View File

@ -0,0 +1,29 @@
builds:
- main: ./cmd/ding/
goos:
- linux
goarch:
- amd64
ldflags:
- -s -w -X gitea.illuad.fr/adrien/ding/cli.version={{.Version}} -X gitea.illuad.fr/adrien/ding/cli.commit={{.Commit}}
buildmode: pie
no_unique_dist_dir: true
archives:
- format: binary
name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}"
checksum:
name_template: "{{ .ProjectName }}-{{ .Version }}-checksums.txt"
algorithm: sha512
extra_files:
- glob: ./LICENSE
- glob: ./README.md
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
sboms:
- artifacts: archive
gitea_urls:
api: https://gitea.illuad.fr/api/v1
download: https://gitea.illuad.fr
skip_tls_verify: false

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Adrien
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

24
Makefile Normal file
View File

@ -0,0 +1,24 @@
.SILENT: ding clean update check docker release
.PHONY: ding clean update check docker release
ding:
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -buildmode=pie -o ding cmd/ding/main.go
clean:
rm --force ding
update:
go get -u ./...
go mod tidy -v
check:
goreportcard-cli -v
govulncheck ./...
GOOS=linux staticcheck -f stylish ./...
docker:
docker build --build-arg VERSION=untagged --build-arg COMMIT=0000000000 -t ding:untagged .
docker run --rm --read-only --cap-drop=all --name ding ding:untagged --help
release:
goreleaser release --clean

130
README.md Normal file
View File

@ -0,0 +1,130 @@
# ding
ding pronounced `[diŋ]`, in the French language is an onomatopoeia evoking the sound produced by the bells of a steeple
or the bell of a front door. ding is a tool for port knocking, hence the name. It took me 10 seconds to find it, be
nice.
For those who haven't heard, port knocking is a method of externally opening ports on a firewall by generating a
connection attempt on a set of prespecified closed ports. Once a correct sequence of connection attempts is received,
the firewall rules are dynamically modified to allow the host which sent the connection attempts to connect over
specific port(s).
ding, your brand-new secure* port knocking client in less than 400 lines of code.
*In its default configuration, ding protects the configuration file by ciphering it via XChaCha20-Poly1305, an
authenticated encryption with additional data (AEAD) algorithm, that combines the XChaCha20 stream cipher with the
Poly1305 message authentication code.
## How to use it
### Setup
The values of the `-t`, `--timeout` or `timeout` and `-d` `--delay` or `delay` flags are of
type [time.Duration](https://pkg.go.dev/time#Duration), which means that the time unit can take on the following
values: `ns`, `us` (or `µs`), `ms`, `s`, `m`, `h`. Respectively: nanosecond, microsecond, millisecond, second, minute
and hour.
By default, and for obvious reasons, the configuration file is ciphered (via XChaCha20-Poly1305). You can disable this
behavior with the `-i` or `--insecure` flag.
Also, the minimum entropy of the password must be 65, you can (at your own risk) easily get around this by using
the `-b` or `--bypass-password-entropy` flag. Note that entropy is only checked during the setup phase.
```shell
$ ding setup --help
```
```
NAME:
ding setup - Launches ding setup
USAGE:
ding setup [command options] [arguments...]
OPTIONS:
--address value, -a value address to knock
--port value, -p value [ --port value, -p value ] ports to knock
--timeout value, -t value timeout in milliseconds (default: 1500ms)
--delay value, -d value delay in milliseconds between knocks (default: 100ms)
--insecure, -i don't de/cipher configuration file (default: false)
--bypass-password-entropy, -b insecurely bypass password entropy (default: false)
--help, -h show help
```
#### Interactive mode
```shell
$ ding setup
? address to knock: 192.168.10.6
? port to knock (separated by commas if several): 38457,22949,9686
? timeout in milliseconds: 1.5s
? delay in milliseconds between knocks: 100ms
? password: *****************
```
#### Non-interactive mode
```shell
$ ding setup -a 192.168.10.6 -p 38457 -p 22949 -p 9686 -t 1500ms -d 100ms
? password: *****************
```
These two approaches boil down to exactly the same thing.
If you go to `$XDG_CONFIG_HOME/ding/` or `$HOME/.config/ding/`, you'll find a file named `.salt` containing the salt
used to derive the 32-byte key used to cipher the configuration file (if you haven't used the `-i` or `--insecure`
flag), as well as the configuration file itself, ciphered or not.
```shell
$ ls -lah ~/.config/ding/
total 16K
drwxr-xr-x 2 adrien users 4.0K Jun 30 17:01 ./
drwxr-xr-x 30 adrien users 4.0K Jun 30 17:01 ../
-rw-r--r-- 1 adrien users 132 Jun 30 17:11 config.toml
-rw-r--r-- 1 adrien users 32 Jun 30 17:11 .salt
```
### Use
```shell
$ ding help
```
```
NAME:
ding - Command line interface tool to knock ports
USAGE:
ding [global options] command [command options] [arguments...]
VERSION:
untagged-0000000000
AUTHOR:
Adrien <contact@illuad.fr>
COMMANDS:
setup, s Launches ding setup
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--insecure, -i don't de/cipher configuration file (default: false)
--help, -h show help
--version, -v print the version
```
It couldn't be simpler. The password is the same as the one entered during the setup phase.
```shell
$ ding
? password: *****************
```
If you add the `-i` or `--insecure` flag when you haven't specified it during the setup step, you'll get an error like
this.
```
2023-07-01T11:09:51+02:00 FTL toml: line 1: invalid UTF-8 byte: 0xc4
```
However, if you've set up ding correctly, you should be able to access your server via SSH.

486
cli/cli.go Normal file
View File

@ -0,0 +1,486 @@
// Package cli defines flags, commands and functions to run.
package cli
import (
"bytes"
"crypto/cipher"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"github.com/AlecAivazis/survey/v2"
"github.com/BurntSushi/toml"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
entropy "github.com/wagslane/go-password-validator"
"golang.org/x/crypto/argon2"
"golang.org/x/crypto/chacha20poly1305"
"net"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
)
// config represents the configuration file
type config struct {
Address string `toml:"address"`
Port []string `toml:"port"`
Timeout time.Duration `toml:"timeout"`
Delay time.Duration `toml:"delay"`
}
// ding holds variables to operate
type ding struct {
cliCtx *cli.Context
config
configFile *os.File
}
const (
appName = "ding"
)
var (
// version and commit can be defined at compilation time via ldflags.
version, commit string
)
func Run() error {
// Use all processors
runtime.GOMAXPROCS(runtime.NumCPU())
// Set global logger
log.Logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}).With().Timestamp().Logger()
// Common flag
var noCipher = &cli.BoolFlag{Name: "insecure", Aliases: []string{"i"}, Usage: "don't de/cipher configuration file", Value: false}
// Initialize ding
app := cli.NewApp()
globalFlags := []cli.Flag{
altsrc.NewStringFlag(&cli.StringFlag{Name: "address", Aliases: []string{"a"}, Usage: "address to knock", Hidden: true}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "port", Aliases: []string{"p"}, Usage: "ports to knock", Hidden: true, Action: func(_ *cli.Context, ports []string) error { return validatePortRange(ports) }}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "timeout", Aliases: []string{"t"}, Usage: "timeout in milliseconds", Hidden: true, DefaultText: "1500ms", Value: 1500 * time.Millisecond}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "delay", Aliases: []string{"d"}, Usage: "delay in milliseconds between knocks", Hidden: true, DefaultText: "100ms", Value: 100 * time.Millisecond}),
noCipher,
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "input configuration file", Hidden: true, DefaultText: "$XDG_CONFIG_HOME/ding/config.toml or $HOME/.config/ding/config.toml"},
}
app.Name = appName
if version == "" || commit == "" {
version = "untagged"
commit = "0000000000"
}
app.Version = version + "-" + commit[0:10]
app.Usage = "Command line interface tool to knock ports"
app.UseShortOptionHandling = true
app.Suggest = true
app.EnableBashCompletion = true
app.Authors = []*cli.Author{
{
Name: "Adrien",
Email: "contact@illuad.fr",
},
}
app.Flags = globalFlags
app.Commands = cli.Commands{
{
Name: "setup",
Aliases: []string{"s"},
Usage: "Launches ding setup",
Flags: []cli.Flag{
&cli.StringFlag{Name: "address", Aliases: []string{"a"}, Usage: "address to knock"},
&cli.StringSliceFlag{Name: "port", Aliases: []string{"p"}, Usage: "ports to knock", Action: func(_ *cli.Context, ports []string) error { return validatePortRange(ports) }},
&cli.DurationFlag{Name: "timeout", Aliases: []string{"t"}, Usage: "timeout in milliseconds", DefaultText: "1500ms", Value: 1500 * time.Millisecond},
&cli.DurationFlag{Name: "delay", Aliases: []string{"d"}, Usage: "delay in milliseconds between knocks", DefaultText: "100ms", Value: 100 * time.Millisecond},
noCipher,
&cli.BoolFlag{Name: "bypass-password-entropy", Aliases: []string{"b"}, Usage: "insecurely bypass password entropy", Value: false}},
Action: func(cliCtx *cli.Context) error {
dingCLI := ding{cliCtx: cliCtx}
dingCLI.
askAddressIfNotSet().
askPortIfNotSet().
askTimeoutIfNotSet().
askDelayIfNotSet()
if cliCtx.Bool("insecure") {
return dingCLI.write()
}
return dingCLI.marshal()
},
},
}
app.Before = func(cliCtx *cli.Context) error {
if cliCtx.IsSet("config") {
return altsrc.InitInputSourceWithContext(cliCtx.App.Flags, altsrc.NewTomlSourceFromFlagFunc("config"))(cliCtx)
}
return nil
}
app.Action = func(cliCtx *cli.Context) error {
dingCLI := ding{cliCtx: cliCtx}
if cliCtx.Bool("insecure") {
if err := dingCLI.read(); err != nil {
return err
}
} else {
if err := dingCLI.unmarshal(); err != nil {
return err
}
}
address := dingCLI.config.Address
if _, err := net.LookupHost(address); err != nil {
return err
}
for _, p := range dingCLI.config.Port {
_, _ = net.DialTimeout("tcp", fmt.Sprintf("%s:%s", address, p), dingCLI.config.Timeout)
time.Sleep(dingCLI.config.Delay)
}
return nil
}
return app.Run(os.Args)
}
// basePath returns configuration path, e.g. /home/jdoe/.config/ding.
// Configuration filename (config.toml) is not added, see configPath for this.
// Empty string is returned if path can't be build (if $XDG_CONFIG_HOME or $HOME are not set).
func (d *ding) basePath() string {
path, ok := os.LookupEnv("XDG_CONFIG_HOME")
if !ok {
// $XDG_CONFIG_HOME is not set
// $HOME is?
path, ok = os.LookupEnv("HOME")
if !ok {
// $HOME is not set
// Base path can't be build
return ""
} else {
// $HOME is set, add ".config/ding"
return filepath.Join(path, ".config", "ding")
}
} else {
// $XDG_CONFIG_HOME is set, add "ding"
return filepath.Join(path, "ding")
}
}
// configPath return configuration file path, e.g. /home/jdoe/.config/ding/config.toml.
// Empty string is returned if configuration file path can't be build (if basePath has returned empty string).
func (d *ding) configPath() string {
path := d.basePath()
if path == "" {
return ""
}
return filepath.Join(path, "config.toml")
}
// saltPath return salt file path, e.g. /home/jdoe/.config/ding/.salt.
// Empty string is returned if salt file path can't be build (if basePath has returned empty string).
func (d *ding) saltPath() string {
path := d.basePath()
if path == "" {
return ""
}
return filepath.Join(path, ".salt")
}
// createBaseFolders creates configuration path, e.g. /home/jdoe/.config/ding.
// Configuration file (config.toml) is not created, see createConfigurationFile for this.
// An error is returned if basePath has returned empty string.
func (d *ding) createBaseFolders() (string, error) {
path := d.basePath()
if path == "" {
return "", errors.New("failed to retrieve base path")
}
return path, os.MkdirAll(path, 0755)
}
// createConfigurationFile creates configuration file, e.g. /home/jdoe/.config/ding/config.toml.
// An error is return if createBaseFolders failed to create base folders or if configuration file creation failed.
func (d *ding) createConfigurationFile() error {
path, err := d.createBaseFolders()
if err != nil {
return err
}
d.configFile, err = os.Create(filepath.Join(path, "config.toml"))
return err
}
// askAddressIfNotSet asks for the address, or sets it if given by flag.
func (d *ding) askAddressIfNotSet() *ding {
if d.cliCtx.IsSet("address") {
d.config.Address = d.cliCtx.String("address")
} else {
if err := survey.AskOne(&survey.Input{
Message: "address to knock:",
}, &d.config.Address); err != nil {
return nil
}
}
return d
}
// askPortIfNotSet asks for the port, or sets it if given by flag.
func (d *ding) askPortIfNotSet() *ding {
if d.cliCtx.IsSet("port") {
d.config.Port = d.cliCtx.StringSlice("port")
} else {
var input string
if err := survey.AskOne(&survey.Input{
Message: "port to knock (separated by commas if several):",
}, &input); err != nil {
return nil
}
d.config.Port = strings.Split(input, ",")
}
return d
}
// askTimeoutIfNotSet asks for the timeout, or sets it if given by flag.
func (d *ding) askTimeoutIfNotSet() *ding {
if d.cliCtx.IsSet("timeout") {
d.config.Timeout = d.cliCtx.Duration("timeout")
} else {
if err := survey.AskOne(&survey.Input{
Message: "timeout in milliseconds:",
Default: d.cliCtx.Duration("timeout").String(),
Help: "TCP connection timeout, this value should be increased on a slow network",
}, &d.config.Timeout); err != nil {
return nil
}
}
return d
}
// askDelayIfNotSet asks for the delay, or sets it if given by flag.
func (d *ding) askDelayIfNotSet() *ding {
if d.cliCtx.IsSet("delay") {
d.config.Delay = d.cliCtx.Duration("delay")
} else {
if err := survey.AskOne(&survey.Input{
Message: "delay in milliseconds between knocks:",
Default: d.cliCtx.Duration("delay").String(),
Help: "waiting time between TCP connections, value tied to server configuration",
}, &d.config.Delay); err != nil {
return nil
}
}
return d
}
// write creates configuration file and encodes config to it.
func (d *ding) write() error {
if err := d.createConfigurationFile(); err != nil {
return err
}
if err := toml.NewEncoder(d.configFile).Encode(d.config); err != nil {
return err
}
return d.configFile.Close()
}
// marshal encodes config to a buffer of bytes suitable for post-manipulations.
// It also generates a 128-bit salt, a 256-bit key derived via Argon2id, initializes a XChaCha20-Poly1305 AEAD cipher.
// Then, the buffer is ciphered and written to the configuration file.
func (d *ding) marshal() error {
if err := d.createConfigurationFile(); err != nil {
return err
}
var dst bytes.Buffer
if err := toml.NewEncoder(&dst).Encode(d.config); err != nil {
return err
}
salt := make([]byte, 16)
if _, err := rand.Read(salt); err != nil {
return nil
}
path := d.saltPath()
if path == "" {
return errors.New("failed to retrieve salt path")
}
encodedSalt := make([]byte, hex.EncodedLen(len(salt)))
hex.Encode(encodedSalt, salt)
if err := os.WriteFile(path, encodedSalt, 0644); err != nil {
return err
}
aead, err := chacha20poly1305.NewX(d.deriveKey(salt))
if err != nil {
return err
}
nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+dst.Len()+aead.Overhead())
if _, err = rand.Read(nonce); err != nil {
return err
}
if _, err = d.configFile.Write(aead.Seal(nonce, nonce, dst.Bytes(), nil)); err != nil {
return err
}
return d.configFile.Close()
}
func (d *ding) askPassword() *bytes.Buffer {
var input string
if err := survey.AskOne(&survey.Password{Message: "password:"}, &input); err != nil {
panic("prompt error:" + err.Error())
}
return bytes.NewBufferString(input)
}
func (d *ding) deriveKey(salt []byte) []byte {
var password *bytes.Buffer
for {
password = d.askPassword()
// Password entropy must be verified only during the setup phase
if d.cliCtx.Command.Name != "setup" {
break
}
if !d.cliCtx.Bool("bypass-password-entropy") {
if err := entropy.Validate(password.String(), 65); err != nil {
log.Warn().Msg(err.Error())
continue
}
}
break
}
defer password.Reset()
return argon2.IDKey(password.Bytes(), salt, 1, 64*1024, uint8(runtime.GOMAXPROCS(runtime.NumCPU())), chacha20poly1305.KeySize)
}
// read reads an unencrypted configuration file.
func (d *ding) read() error {
path := d.configPath()
if path == "" {
return errors.New("failed to retrieve configuration file path")
}
var err error
d.configFile, err = os.Open(path)
if err != nil {
return err
}
defer d.configFile.Close()
_, err = toml.NewDecoder(d.configFile).Decode(&d.config)
return err
}
// unmarshal deciphers and decodes the configuration file.
func (d *ding) unmarshal() error {
path := d.configPath()
if path == "" {
return errors.New("failed to retrieve configuration file path")
}
conf, err := os.ReadFile(path)
if err != nil {
return err
}
path = d.saltPath()
if path == "" {
return errors.New("failed to retrieve salt path")
}
var salt []byte
salt, err = os.ReadFile(path)
if err != nil {
return err
}
decodedSalt := make([]byte, hex.DecodedLen(len(salt)))
if _, err = hex.Decode(decodedSalt, salt); err != nil {
return err
}
var aead cipher.AEAD
aead, err = chacha20poly1305.NewX(d.deriveKey(decodedSalt))
if err != nil {
return err
}
if len(conf) < aead.NonceSize() {
return errors.New("ciphered data size is greater than the nonce size")
}
nonce, cipheredData := conf[:aead.NonceSize()], conf[aead.NonceSize():]
var decipheredData []byte
decipheredData, err = aead.Open(nil, nonce, cipheredData, nil)
if err != nil {
return err
}
_, err = toml.NewDecoder(bytes.NewBuffer(decipheredData)).Decode(&d.config)
return err
}
func validatePortRange(ports []string) error {
for _, port := range ports {
p, err := strconv.Atoi(port)
if err != nil {
return err
}
if p < 1 || p > 65535 {
return fmt.Errorf("%d: invalid port", p)
}
}
return nil
}

12
cmd/ding/main.go Normal file
View File

@ -0,0 +1,12 @@
package main
import (
"gitea.illuad.fr/adrien/ding/cli"
"github.com/rs/zerolog/log"
)
func main() {
if err := cli.Run(); err != nil {
log.Fatal().Msg(err.Error())
}
}

26
go.mod Normal file
View File

@ -0,0 +1,26 @@
module gitea.illuad.fr/adrien/ding
go 1.20
require (
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/BurntSushi/toml v1.3.2
github.com/rs/zerolog v1.29.1
github.com/urfave/cli/v2 v2.25.7
github.com/wagslane/go-password-validator v0.3.0
golang.org/x/crypto v0.10.0
)
require (
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
golang.org/x/sys v0.9.0 // indirect
golang.org/x/term v0.9.0 // indirect
golang.org/x/text v0.10.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

90
go.sum Normal file
View File

@ -0,0 +1,90 @@
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I=
github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28=
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=