├── config └── config.go ├── .github ├── pull_request_template.md └── workflows │ ├── pr-title.yml │ ├── gosec.yml │ ├── pr-labels.yml │ ├── pr-validation.yml │ └── release.yml ├── reporting ├── yaml.go ├── json.go ├── report_test.go ├── report.go └── sarif.go ├── .gitignore ├── Makefile ├── Dockerfile ├── plugins ├── plugins.go ├── git.go ├── filesystem.go ├── slack_test.go ├── slack.go ├── discord.go ├── confluence.go └── paligo.go ├── main.go ├── lib ├── http.go ├── flags.go └── flags_test.go ├── go.mod ├── secrets ├── secrets_test.go └── secrets.go ├── README.md ├── cmd └── main.go ├── LICENSE └── go.sum /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Config struct { 4 | Name string 5 | Version string 6 | } 7 | 8 | func LoadConfig(name string, version string) *Config { 9 | return &Config{Name: name, Version: version} 10 | } 11 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | Closes # 6 | 7 | **Proposed Changes** 8 | - 9 | - 10 | - 11 | 12 | I submit this contribution under the Apache-2.0 license. 13 | -------------------------------------------------------------------------------- /reporting/yaml.go: -------------------------------------------------------------------------------- 1 | package reporting 2 | 3 | import ( 4 | "gopkg.in/yaml.v2" 5 | "log" 6 | ) 7 | 8 | func writeYaml(report Report) string { 9 | yamlReport, err := yaml.Marshal(&report) 10 | if err != nil { 11 | log.Fatalf("failed to create Yaml report with error: %v", err) 12 | } 13 | 14 | return string(yamlReport) 15 | } 16 | -------------------------------------------------------------------------------- /reporting/json.go: -------------------------------------------------------------------------------- 1 | package reporting 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | ) 7 | 8 | func writeJson(report Report) string { 9 | jsonReport, err := json.MarshalIndent(report, "", " ") 10 | if err != nil { 11 | log.Fatalf("failed to create Json report with error: %v", err) 12 | } 13 | 14 | return string(jsonReport) 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | .idea 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | vendor/ 16 | 17 | # IDE directories and files 18 | .vscode/ 19 | 20 | 2ms 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | image_label ?= latest 2 | image_name ?= checkmarx/2ms:$(image_label) 3 | image_file_name ?= checkmarx-2ms-$(image_label).tar 4 | 5 | build: 6 | docker build -t $(image_name) . 7 | 8 | save: build 9 | docker save $(image_name) > $(image_file_name) 10 | 11 | run: 12 | docker run -it $(image_name) $(ARGS) 13 | 14 | # To run golangci-lint, you need to install it first: https://golangci-lint.run/usage/install/#local-installation 15 | lint: 16 | golangci-lint run -v -E gofmt --timeout=5m 17 | lint-fix: 18 | golangci-lint run -v -E gofmt --fix --timeout=5m -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Builder image 2 | FROM golang:1.20.5-alpine3.18 AS builder 3 | 4 | WORKDIR /app 5 | 6 | COPY go.mod go.sum ./ 7 | RUN go mod download 8 | 9 | COPY . . 10 | RUN go build -o /app/2ms . 11 | 12 | # Runtime image 13 | # kics-scan disable=b03a748a-542d-44f4-bb86-9199ab4fd2d5 14 | # ^^^^ disable kics Healthcheck result 15 | FROM alpine:3.18 16 | 17 | RUN apk add --no-cache git=2.40.1-r0 18 | 19 | RUN addgroup -S 2ms && adduser -S 2ms -G 2ms 20 | USER 2ms 21 | 22 | RUN git config --global --add safe.directory /repo 23 | 24 | COPY --from=builder /app/2ms /2ms 25 | ENTRYPOINT ["/2ms"] 26 | 27 | -------------------------------------------------------------------------------- /plugins/plugins.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | type Item struct { 10 | Content string 11 | // Unique identifier of the item (page, document, file) with user-friendly content (e.g. URL, file path) 12 | ID string 13 | } 14 | 15 | type Plugin struct { 16 | ID string 17 | Limit chan struct{} 18 | } 19 | 20 | type Channels struct { 21 | Items chan Item 22 | Errors chan error 23 | WaitGroup *sync.WaitGroup 24 | } 25 | 26 | type IPlugin interface { 27 | GetName() string 28 | DefineCommand(channels Channels) (*cobra.Command, error) 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/pr-title.yml: -------------------------------------------------------------------------------- 1 | name: Validate Conventional Commit title 2 | 3 | on: 4 | pull_request: 5 | types: [opened, edited, synchronize, reopened] 6 | 7 | jobs: 8 | validate: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: install commitlint 12 | run: npm install -g @commitlint/cli @commitlint/config-conventional 13 | - name: config commitlint 14 | run: | 15 | echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js 16 | - name: validate PR title 17 | run: | 18 | echo ${{ github.event.pull_request.title }} | commitlint 19 | -------------------------------------------------------------------------------- /.github/workflows/gosec.yml: -------------------------------------------------------------------------------- 1 | name: gosec 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | schedule: 11 | - cron: "0 0 * * *" 12 | 13 | jobs: 14 | gosec: 15 | runs-on: ubuntu-latest 16 | env: 17 | GO111MODULE: on 18 | steps: 19 | - name: Checkout Source 20 | uses: actions/checkout@v3 21 | - name: Run Gosec Security Scanner 22 | uses: securego/gosec@master 23 | with: 24 | args: "-no-fail -fmt sarif -out results.sarif ./..." 25 | - name: Upload Gosec Results 26 | uses: github/codeql-action/upload-sarif@v2 27 | with: 28 | sarif_file: results.sarif 29 | -------------------------------------------------------------------------------- /.github/workflows/pr-labels.yml: -------------------------------------------------------------------------------- 1 | name: PR Labels 2 | 3 | on: 4 | pull_request_target: 5 | types: [opened] 6 | 7 | jobs: 8 | mark_as_community: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | pull-requests: write 12 | steps: 13 | - name: Mark as Community if PR is from a fork 14 | if: github.event.pull_request.head.repo.full_name != github.repository 15 | uses: actions/github-script@v6 16 | with: 17 | script: | 18 | github.rest.issues.addLabels({ 19 | issue_number: context.issue.number, 20 | owner: context.repo.owner, 21 | repo: context.repo.repo, 22 | labels: ['Community'] 23 | }) 24 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | 7 | "github.com/checkmarx/2ms/cmd" 8 | 9 | "github.com/rs/zerolog" 10 | "github.com/rs/zerolog/log" 11 | ) 12 | 13 | func main() { 14 | zerolog.SetGlobalLevel(zerolog.InfoLevel) 15 | // send all logs to stdout 16 | log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: "15:04:05"}) 17 | 18 | // this block sets up a go routine to listen for an interrupt signal 19 | // which will immediately exit gitleaks 20 | stopChan := make(chan os.Signal, 1) 21 | signal.Notify(stopChan, os.Interrupt) 22 | go listenForInterrupt(stopChan) 23 | cmd.Execute() 24 | } 25 | 26 | func listenForInterrupt(stopScan chan os.Signal) { 27 | <-stopScan 28 | log.Fatal().Msg("Interrupt signal received. Exiting...") 29 | } 30 | -------------------------------------------------------------------------------- /lib/http.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | ) 9 | 10 | type ICredentials interface { 11 | GetCredentials() (string, string) 12 | } 13 | 14 | func CreateBasicAuthCredentials(credentials ICredentials) string { 15 | username, password := credentials.GetCredentials() 16 | return "Basic " + base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password))) 17 | } 18 | 19 | type IAuthorizationHeader interface { 20 | GetAuthorizationHeader() string 21 | } 22 | 23 | func HttpRequest(method string, url string, autherization IAuthorizationHeader) ([]byte, *http.Response, error) { 24 | request, err := http.NewRequest(method, url, nil) 25 | if err != nil { 26 | return nil, nil, fmt.Errorf("unexpected error creating an http request %w", err) 27 | } 28 | 29 | if autherization.GetAuthorizationHeader() != "" { 30 | request.Header.Set("Authorization", autherization.GetAuthorizationHeader()) 31 | } 32 | 33 | client := &http.Client{} 34 | response, err := client.Do(request) 35 | if err != nil { 36 | return nil, response, fmt.Errorf("unable to send http request %w", err) 37 | } 38 | 39 | defer response.Body.Close() 40 | 41 | if response.StatusCode < 200 || response.StatusCode >= 300 { 42 | return nil, response, fmt.Errorf("error calling http url \"%v\". status code: %v", url, response) 43 | } 44 | 45 | body, err := io.ReadAll(response.Body) 46 | if err != nil { 47 | return nil, response, fmt.Errorf("unexpected error reading http response body %w", err) 48 | } 49 | 50 | return body, response, nil 51 | } 52 | -------------------------------------------------------------------------------- /reporting/report_test.go: -------------------------------------------------------------------------------- 1 | package reporting 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestAddSecretToFile(t *testing.T) { 9 | secretValue := string(` 10 | -----BEGIN RSA PRIVATE KEY----- 11 | MIICWwIBAAKBgQCKLwIHewTIhcpH3WLnxZ61xBAk2lnkdahFxjHYi+khrENzbGr8 12 | EeJDZ1FMUDDYGeLtjlROLHT41ovicFbsmgIU0QQVFewIAwvKIw5hBtq0TtO9CsXe 13 | BaNmzw8ZduXJ/clOpdOF7/1ro485a+v956ZAhB2ohbk6qRqGyg3kaxclOQIDAQAB 14 | AoGAV7z5QN6vbtLkWTUMc7VazHas+Xla0mCSc5sgUyqi4CqMuWEBnQON8tZLHHVe 15 | ThhBqixRA0HfE5DGSQSjbJ9s6fD+Sjt0Qj2yer70FuEiR0uGM4tOAE7WbX+Ny7PT 16 | gmDiWOITe7v0yzIgZzbLgPhg5SlCmiy8Nv2Zf/v54yLVPLECQQDbwpsuu6beMDip 17 | kRB/msCAEEAstdfSPY8L9QySYxskkJvtWpWBu5trnRatiGoLYWvnsBzcL4xWGrs8 18 | Tpr4hTirAkEAoPiRDHrVbkKAgrmLW/TrSDiOG8uXSTuvz4iFgzCG6Cd8bp7mDKhJ 19 | l98Upelf0Is5sEnLDqnFl62LZAyckeThqwJAOjZChQ6QFSsQ11nl1OdZNpMXbMB+ 20 | euJzkedHfT9jYTwtEaJ9F/BqKwdhinYoIPudabHs8yZlNim+jysDQfGIIQJAGqlx 21 | JPcHeO7M6FohKgcEHX84koQDN98J/L7pFlSoU7WOl6f8BKavIdeSTPS9qQYWdQuT 22 | 9YbLMpdNGjI4kLWvZwJAJt8Qnbc2ZfS0ianwphoOdB0EwOMKNygjnYx7VoqR9/h1 23 | 4Xgur9w/aLZrLM3DSatR+kL+cVTyDTtgCt9Dc8k48Q== 24 | -----END RSA PRIVATE KEY-----`) 25 | 26 | results := map[string][]Secret{} 27 | report := Report{len(results), 1, results} 28 | secret := Secret{Description: "bla", StartLine: 0, StartColumn: 0, EndLine: 0, EndColumn: 0, Value: secretValue} 29 | source := "directory\\rawStringAsFile.txt" 30 | 31 | report.Results[source] = append(report.Results[source], secret) 32 | 33 | key, fileExist := report.Results[source] 34 | if !fileExist { 35 | t.Errorf("key %s not added", source) 36 | } 37 | 38 | if !reflect.DeepEqual(report.Results, results) { 39 | t.Errorf("got %q want %q", key, results) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /reporting/report.go: -------------------------------------------------------------------------------- 1 | package reporting 2 | 3 | import ( 4 | "fmt" 5 | "github.com/checkmarx/2ms/config" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | const ( 12 | jsonFormat = "json" 13 | yamlFormat = "yaml" 14 | sarifFormat = "sarif" 15 | ) 16 | 17 | type Report struct { 18 | TotalItemsScanned int `json:"totalItemsScanned"` 19 | TotalSecretsFound int `json:"totalSecretsFound"` 20 | Results map[string][]Secret `json:"results"` 21 | } 22 | 23 | type Secret struct { 24 | ID string `json:"id"` 25 | Source string `json:"source"` 26 | Description string `json:"description"` 27 | StartLine int `json:"startLine"` 28 | EndLine int `json:"endLine"` 29 | StartColumn int `json:"startColumn"` 30 | EndColumn int `json:"endColumn"` 31 | Value string `json:"value"` 32 | } 33 | 34 | func Init() *Report { 35 | return &Report{ 36 | Results: make(map[string][]Secret), 37 | } 38 | } 39 | 40 | func (r *Report) ShowReport(format string, cfg *config.Config) { 41 | output := r.getOutput(format, cfg) 42 | 43 | fmt.Println("Summary:") 44 | fmt.Print(output) 45 | } 46 | 47 | func (r *Report) WriteFile(reportPath []string, cfg *config.Config) error { 48 | for _, path := range reportPath { 49 | file, err := os.Create(path) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | fileExtension := filepath.Ext(path) 55 | format := strings.TrimPrefix(fileExtension, ".") 56 | output := r.getOutput(format, cfg) 57 | 58 | _, err = file.WriteString(output) 59 | if err != nil { 60 | return err 61 | } 62 | } 63 | return nil 64 | } 65 | 66 | func (r *Report) getOutput(format string, cfg *config.Config) string { 67 | var output string 68 | switch format { 69 | case jsonFormat: 70 | output = writeJson(*r) 71 | case yamlFormat: 72 | output = writeYaml(*r) 73 | case sarifFormat: 74 | output = writeSarif(*r, cfg) 75 | } 76 | return output 77 | } 78 | -------------------------------------------------------------------------------- /.github/workflows/pr-validation.yml: -------------------------------------------------------------------------------- 1 | name: PR Validation 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | test: 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest] 13 | 14 | runs-on: ${{ matrix.os }} 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v3 19 | 20 | - uses: actions/setup-go@v4 21 | with: 22 | go-version: "^1.20" 23 | 24 | - name: Go Linter 25 | run: docker run --rm -v $(pwd):/app -w /app golangci/golangci-lint:v1.52.0 golangci-lint run -v -E gofmt --timeout=5m 26 | 27 | - name: Go Test 28 | run: go test -v ./... 29 | 30 | build: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Checkout code 34 | uses: actions/checkout@v3 35 | 36 | - name: Set up Docker Buildx 37 | uses: docker/setup-buildx-action@v2 38 | 39 | - run: make build 40 | - name: Run docker and check its output 41 | run: 42 | if docker run -v "$(pwd)":/repo -t checkmarx/2ms:latest git /repo | grep -A 5 "Summary:"; then 43 | echo "Docker ran as expected"; 44 | else 45 | echo "Docker did not run as expected"; 46 | exit 1; 47 | fi 48 | 49 | kics: 50 | runs-on: ubuntu-latest 51 | steps: 52 | - uses: actions/checkout@v3 53 | - run: mkdir -p kics-results 54 | 55 | - name: Run KICS scan 56 | uses: checkmarx/kics-github-action@master 57 | with: 58 | path: Dockerfile 59 | output_path: kics-results 60 | output_formats: json,sarif 61 | enable_comments: ${{ github.event_name == 'pull_request'}} 62 | fail_on: high,medium 63 | - name: Show KICS results 64 | if: failure() 65 | run: cat kics-results/results.json 66 | # - name: Upload SARIF file 67 | # uses: github/codeql-action/upload-sarif@v2 68 | # with: 69 | # sarif_file: kics-results/results.sarif 70 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/checkmarx/2ms 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/bwmarrin/discordgo v0.27.1 7 | github.com/rs/zerolog v1.29.0 8 | github.com/slack-go/slack v0.12.2 9 | github.com/spf13/cobra v1.6.1 10 | github.com/stretchr/testify v1.8.1 11 | github.com/zricethezav/gitleaks/v8 v8.16.1 12 | golang.org/x/time v0.1.0 13 | gopkg.in/yaml.v2 v2.4.0 14 | ) 15 | 16 | require ( 17 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 18 | github.com/charmbracelet/lipgloss v0.7.1 // indirect 19 | github.com/davecgh/go-spew v1.1.1 // indirect 20 | github.com/fatih/semgroup v1.2.0 // indirect 21 | github.com/fsnotify/fsnotify v1.6.0 // indirect 22 | github.com/gitleaks/go-gitdiff v0.8.0 // indirect 23 | github.com/gorilla/websocket v1.5.0 // indirect 24 | github.com/h2non/filetype v1.1.3 // indirect 25 | github.com/hashicorp/hcl v1.0.0 // indirect 26 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 27 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 28 | github.com/lucasjones/reggen v0.0.0-20200904144131-37ba4fa293bb // indirect 29 | github.com/magiconair/properties v1.8.7 // indirect 30 | github.com/mattn/go-colorable v0.1.13 // indirect 31 | github.com/mattn/go-isatty v0.0.17 // indirect 32 | github.com/mattn/go-runewidth v0.0.14 // indirect 33 | github.com/mitchellh/mapstructure v1.5.0 // indirect 34 | github.com/muesli/reflow v0.3.0 // indirect 35 | github.com/muesli/termenv v0.15.1 // indirect 36 | github.com/pelletier/go-toml/v2 v2.0.7 // indirect 37 | github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9 // indirect 38 | github.com/pmezard/go-difflib v1.0.0 // indirect 39 | github.com/rivo/uniseg v0.4.4 // indirect 40 | github.com/spf13/afero v1.9.5 // indirect 41 | github.com/spf13/cast v1.5.0 // indirect 42 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 43 | github.com/spf13/pflag v1.0.5 // indirect 44 | github.com/spf13/viper v1.15.0 // indirect 45 | github.com/subosito/gotenv v1.4.2 // indirect 46 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect 47 | golang.org/x/sync v0.1.0 // indirect 48 | golang.org/x/sys v0.6.0 // indirect 49 | golang.org/x/text v0.8.0 // indirect 50 | gopkg.in/ini.v1 v1.67.0 // indirect 51 | gopkg.in/yaml.v3 v3.0.1 // indirect 52 | ) 53 | -------------------------------------------------------------------------------- /secrets/secrets_test.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestLoadAllRules(t *testing.T) { 8 | rules, _ := loadAllRules() 9 | 10 | if len(rules) <= 1 { 11 | t.Error("no rules were loaded") 12 | } 13 | } 14 | 15 | func TestIsAllFilter_AllFilterNotPresent(t *testing.T) { 16 | filters := []string{"token", "key"} 17 | 18 | isAllFilter := isAllFilter(filters) 19 | 20 | if isAllFilter { 21 | t.Errorf("all rules were not selected") 22 | } 23 | } 24 | 25 | func TestIsAllFilter_AllFilterPresent(t *testing.T) { 26 | filters := []string{"token", "key", "all"} 27 | 28 | isAllFilter := isAllFilter(filters) 29 | 30 | if !isAllFilter { 31 | t.Errorf("all filter selected") 32 | } 33 | } 34 | 35 | func TestIsAllFilter_OnlyAll(t *testing.T) { 36 | filters := []string{"all"} 37 | 38 | isAllFilter := isAllFilter(filters) 39 | 40 | if !isAllFilter { 41 | t.Errorf("all filter selected") 42 | } 43 | } 44 | 45 | func TestGetRules_AllFilter(t *testing.T) { 46 | rules, _ := loadAllRules() 47 | tags := []string{"all"} 48 | 49 | filteredRules := getRules(rules, tags) 50 | 51 | if len(filteredRules) <= 1 { 52 | t.Error("no rules were loaded") 53 | } 54 | } 55 | 56 | func TestGetRules_TokenFilter(t *testing.T) { 57 | rules, _ := loadAllRules() 58 | tags := []string{"api-token"} 59 | 60 | filteredRules := getRules(rules, tags) 61 | 62 | if len(filteredRules) <= 1 { 63 | t.Error("no rules were loaded") 64 | } 65 | } 66 | 67 | func TestGetRules_KeyFilter(t *testing.T) { 68 | rules, _ := loadAllRules() 69 | filters := []string{"api-key"} 70 | 71 | filteredRules := getRules(rules, filters) 72 | 73 | if len(filteredRules) <= 1 { 74 | t.Error("no rules were loaded") 75 | } 76 | } 77 | 78 | func TestGetRules_IdFilter(t *testing.T) { 79 | rules, _ := loadAllRules() 80 | filters := []string{"access-token"} 81 | 82 | filteredRules := getRules(rules, filters) 83 | 84 | if len(filteredRules) <= 1 { 85 | t.Error("no rules were loaded") 86 | } 87 | } 88 | 89 | func TestGetRules_IdAndKeyFilters(t *testing.T) { 90 | rules, _ := loadAllRules() 91 | filters := []string{"api-key", "access-token"} 92 | 93 | filteredRules := getRules(rules, filters) 94 | 95 | if len(filteredRules) <= 1 { 96 | t.Error("no rules were loaded") 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /plugins/git.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/gitleaks/go-gitdiff/gitdiff" 8 | "github.com/rs/zerolog/log" 9 | "github.com/spf13/cobra" 10 | "github.com/zricethezav/gitleaks/v8/detect/git" 11 | ) 12 | 13 | type GitPlugin struct { 14 | Plugin 15 | Channels 16 | } 17 | 18 | func (p *GitPlugin) GetName() string { 19 | return "git" 20 | } 21 | 22 | func (p *GitPlugin) DefineCommand(channels Channels) (*cobra.Command, error) { 23 | p.Channels = channels 24 | 25 | command := &cobra.Command{ 26 | Use: fmt.Sprintf("%s ", p.GetName()), 27 | Short: "Scan Git repository", 28 | Long: "Scan Git repository for sensitive information.", 29 | Args: cobra.MatchAll(cobra.ExactArgs(1), validGitRepoArgs), 30 | Run: func(cmd *cobra.Command, args []string) { 31 | log.Info().Msg("Git plugin started") 32 | scanGit(args[0], channels.Items, channels.Errors) 33 | }, 34 | } 35 | 36 | return command, nil 37 | } 38 | 39 | func scanGit(path string, itemsChan chan Item, errChan chan error) { 40 | fileChan, err := git.GitLog(path, "") 41 | if err != nil { 42 | errChan <- fmt.Errorf("error while scanning git repository: %w", err) 43 | } 44 | log.Debug().Msgf("scanned git repository: %s", path) 45 | 46 | for file := range fileChan { 47 | log.Debug().Msgf("file: %s; Commit: %s", file.NewName, file.PatchHeader.Title) 48 | if file.IsBinary || file.IsDelete { 49 | continue 50 | } 51 | 52 | fileChanges := "" 53 | for _, textFragment := range file.TextFragments { 54 | if textFragment != nil { 55 | raw := textFragment.Raw(gitdiff.OpAdd) 56 | fileChanges += raw 57 | } 58 | } 59 | if fileChanges != "" { 60 | itemsChan <- Item{ 61 | Content: fileChanges, 62 | ID: fmt.Sprintf("git show %s:%s", file.PatchHeader.SHA, file.NewName), 63 | } 64 | } 65 | } 66 | } 67 | 68 | func validGitRepoArgs(cmd *cobra.Command, args []string) error { 69 | stat, err := os.Stat(args[0]) 70 | if err != nil { 71 | return err 72 | } 73 | if !stat.IsDir() { 74 | return fmt.Errorf("%s is not a directory", args[0]) 75 | } 76 | gitFolder := fmt.Sprintf("%s/.git", args[0]) 77 | stat, err = os.Stat(gitFolder) 78 | if err != nil { 79 | return err 80 | } 81 | if !stat.IsDir() { 82 | return fmt.Errorf("%s is not a git repository", args[0]) 83 | } 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /lib/flags.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/rs/zerolog/log" 9 | "github.com/spf13/cobra" 10 | "github.com/spf13/pflag" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | func LoadConfig(v *viper.Viper, configFilePath string) error { 15 | if configFilePath == "" { 16 | return nil 17 | } 18 | 19 | configType := strings.TrimPrefix(filepath.Ext(configFilePath), ".") 20 | 21 | v.SetConfigType(configType) 22 | v.SetConfigFile(configFilePath) 23 | return v.ReadInConfig() 24 | } 25 | 26 | // TODO: can be a package 27 | // BindFlags fill flags values with config file or environment variables data 28 | func BindFlags(cmd *cobra.Command, v *viper.Viper, envPrefix string) error { 29 | commandHierarchy := getCommandHierarchy(cmd) 30 | 31 | bindFlag := func(f *pflag.Flag) { 32 | fullFlagName := fmt.Sprintf("%s%s", commandHierarchy, f.Name) 33 | bindEnvVarIntoViper(v, fullFlagName, envPrefix) 34 | 35 | if f.Changed { 36 | return 37 | } 38 | 39 | if v.IsSet(fullFlagName) { 40 | val := v.Get(fullFlagName) 41 | applyViperFlagToCommand(f, val, cmd) 42 | } 43 | } 44 | cmd.PersistentFlags().VisitAll(bindFlag) 45 | cmd.Flags().VisitAll(bindFlag) 46 | 47 | for _, subCmd := range cmd.Commands() { 48 | if err := BindFlags(subCmd, v, envPrefix); err != nil { 49 | return err 50 | } 51 | } 52 | 53 | return nil 54 | } 55 | 56 | func bindEnvVarIntoViper(v *viper.Viper, fullFlagName, envPrefix string) { 57 | envVarSuffix := strings.ToUpper(strings.ReplaceAll(strings.ReplaceAll(fullFlagName, "-", "_"), ".", "_")) 58 | envVarName := fmt.Sprintf("%s_%s", envPrefix, envVarSuffix) 59 | 60 | if err := v.BindEnv(fullFlagName, envVarName, strings.ToLower(envVarName)); err != nil { 61 | log.Err(err).Msg("Failed to bind Viper flags") 62 | } 63 | } 64 | 65 | func applyViperFlagToCommand(flag *pflag.Flag, val interface{}, cmd *cobra.Command) { 66 | switch t := val.(type) { 67 | case []interface{}: 68 | var paramSlice []string 69 | for _, param := range t { 70 | paramSlice = append(paramSlice, param.(string)) 71 | } 72 | valStr := strings.Join(paramSlice, ",") 73 | if err := flag.Value.Set(valStr); err != nil { 74 | log.Err(err).Msg("Failed to set Viper flags") 75 | } 76 | default: 77 | newVal := fmt.Sprintf("%v", val) 78 | if err := flag.Value.Set(newVal); err != nil { 79 | log.Err(err).Msg("Failed to set Viper flags") 80 | } 81 | } 82 | flag.Changed = true 83 | } 84 | 85 | func getCommandHierarchy(cmd *cobra.Command) string { 86 | names := []string{} 87 | if !cmd.HasParent() { 88 | return "" 89 | } 90 | 91 | for parent := cmd; parent.HasParent() && parent.Name() != ""; parent = parent.Parent() { 92 | names = append([]string{parent.Name()}, names...) 93 | } 94 | 95 | if len(names) == 0 { 96 | return "" 97 | } 98 | 99 | return strings.Join(names, ".") + "." 100 | } 101 | -------------------------------------------------------------------------------- /plugins/filesystem.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "sync" 8 | 9 | "github.com/rs/zerolog/log" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | const flagFolder = "path" 14 | const flagIgnored = "ignore" 15 | 16 | var ignoredFolders = []string{".git"} 17 | 18 | type FileSystemPlugin struct { 19 | Plugin 20 | Path string 21 | Ignored []string 22 | } 23 | 24 | func (p *FileSystemPlugin) GetName() string { 25 | return "filesystem" 26 | } 27 | 28 | func (p *FileSystemPlugin) DefineCommand(channels Channels) (*cobra.Command, error) { 29 | var cmd = &cobra.Command{ 30 | Use: fmt.Sprintf("%s --%s PATH", p.GetName(), flagFolder), 31 | Short: "Scan local folder", 32 | Long: "Scan local folder for sensitive information", 33 | Run: func(cmd *cobra.Command, args []string) { 34 | log.Info().Msg("Folder plugin started") 35 | p.getFiles(channels.Items, channels.Errors, channels.WaitGroup) 36 | }, 37 | } 38 | 39 | flags := cmd.Flags() 40 | flags.StringVar(&p.Path, flagFolder, "", "Local folder path [required]") 41 | if err := cmd.MarkFlagDirname(flagFolder); err != nil { 42 | return nil, fmt.Errorf("error while marking '%s' flag as directory: %w", flagFolder, err) 43 | } 44 | if err := cmd.MarkFlagRequired(flagFolder); err != nil { 45 | return nil, fmt.Errorf("error while marking '%s' flag as required: %w", flagFolder, err) 46 | } 47 | 48 | flags.StringSliceVar(&p.Ignored, flagIgnored, []string{}, "Patterns to ignore") 49 | 50 | return cmd, nil 51 | } 52 | 53 | func (p *FileSystemPlugin) getFiles(items chan Item, errs chan error, wg *sync.WaitGroup) { 54 | fileList := make([]string, 0) 55 | err := filepath.Walk(p.Path, func(path string, fInfo os.FileInfo, err error) error { 56 | if err != nil { 57 | log.Fatal().Err(err).Msg("error while walking through the directory") 58 | } 59 | for _, ignoredFolder := range ignoredFolders { 60 | if fInfo.Name() == ignoredFolder && fInfo.IsDir() { 61 | return filepath.SkipDir 62 | } 63 | } 64 | for _, ignoredPattern := range p.Ignored { 65 | matched, err := filepath.Match(ignoredPattern, filepath.Base(path)) 66 | if err != nil { 67 | return err 68 | } 69 | if matched && fInfo.IsDir() { 70 | return filepath.SkipDir 71 | } 72 | if matched { 73 | return nil 74 | } 75 | } 76 | if fInfo.Size() == 0 { 77 | return nil 78 | } 79 | if !fInfo.IsDir() { 80 | fileList = append(fileList, path) 81 | } 82 | return err 83 | }) 84 | 85 | if err != nil { 86 | log.Fatal().Err(err).Msg("error while walking through the directory") 87 | } 88 | 89 | p.getItems(items, errs, wg, fileList) 90 | } 91 | 92 | func (p *FileSystemPlugin) getItems(items chan Item, errs chan error, wg *sync.WaitGroup, fileList []string) { 93 | for _, filePath := range fileList { 94 | wg.Add(1) 95 | go func(filePath string) { 96 | defer wg.Done() 97 | actualFile, err := p.getItem(wg, filePath) 98 | if err != nil { 99 | errs <- err 100 | return 101 | } 102 | items <- *actualFile 103 | }(filePath) 104 | } 105 | } 106 | 107 | func (p *FileSystemPlugin) getItem(wg *sync.WaitGroup, filePath string) (*Item, error) { 108 | b, err := os.ReadFile(filePath) 109 | if err != nil { 110 | return nil, err 111 | } 112 | 113 | content := &Item{ 114 | Content: string(b), 115 | ID: filePath, 116 | } 117 | return content, nil 118 | } 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Latest Release](https://img.shields.io/github/v/release/checkmarx/2ms)](https://github.com/checkmarx/2ms/releases) 2 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 3 | [![GitHub Discussions](https://img.shields.io/badge/chat-discussions-blue.svg?style=flat-square)](https://github.com/Checkmarx/2ms/discussions) 4 | 5 | ![2ms Mascot](https://github.com/Checkmarx/2ms/assets/1287098/3a543045-9c6a-4a35-9bf8-f41919e7b03e) 6 | 7 | Too many secrets (2MS) is an open source project dedicated to helping people protect their sensitive information like passwords, API keys from appearing in public websites and communication services. 8 | 9 | During the software development lifecycle (SDLC), developers ofen communicate and exchange secret data in various ways. While there are tools available for detecting secrets in source code and Git repositories, there are few options for identifying secrets in plain text documents, emails, chat logs, content managment systems and more. Some of them are public, or have a mixture of private / public, meaning it's easy to make an onest mistake and publish secret data to the world wide web. 10 | 11 | 2ms is built over a secret detection engine (currently [gitleaks](https://github.com/gitleaks/gitleaks)) and includes various plugins to interact with popular platforms. This means anyone can contribute, improve and extend 2ms quite easily. We believe that by working together, we can create a more secure digital world. You're welcome to join our [community](https://github.com/Checkmarx/2ms/discussions). 12 | 13 | ## Supported Platforms 14 | 15 | - Confluence 16 | - Discord 17 | - Slack 18 | - Git 19 | - Paligo 20 | - Local directory / files 21 | 22 | ## Getting 2ms 23 | 24 | ``` 25 | # go install github.com/checkmarx/2ms@latest 26 | ``` 27 | 28 | ### Docker 29 | 30 | ``` 31 | docker run -v path/to/my/repo:/repo checkmarx/2ms git /repo 32 | ``` 33 | 34 | (For `git` command, you have to mount your git repository to `/repo` inside the container) 35 | 36 | ## Getting started 37 | 38 | 39 | ``` 40 | 2ms Secrets Detection: A tool to detect secrets in public websites and communication services. 41 | 42 | Usage: 43 | 2ms [command] 44 | 45 | Commands 46 | confluence Scan Confluence server 47 | discord Scan Discord server 48 | filesystem Scan local folder 49 | git Scan Git repository 50 | paligo Scan Paligo instance 51 | slack Scan Slack team 52 | 53 | Additional Commands: 54 | completion Generate the autocompletion script for the specified shell 55 | help Help about any command 56 | 57 | Flags: 58 | --config string config file path 59 | -h, --help help for 2ms 60 | --log-level string log level (trace, debug, info, warn, error, fatal) (default "info") 61 | --regex stringArray custom regexes to apply to the scan, must be valid Go regex 62 | --report-path strings path to generate report files. The output format will be determined by the file extension (.json, .yaml, .sarif) 63 | --stdout-format string stdout output format, available formats are: json, yaml, sarif (default "yaml") 64 | --tags strings select rules to be applied (default [all]) 65 | -v, --version version for 2ms 66 | 67 | Use "2ms [command] --help" for more information about a command. 68 | ``` 69 | 70 | 71 | --- 72 | 73 | Made by Checkmarx with :heart: 74 | -------------------------------------------------------------------------------- /reporting/sarif.go: -------------------------------------------------------------------------------- 1 | package reporting 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/checkmarx/2ms/config" 7 | "log" 8 | ) 9 | 10 | func writeSarif(report Report, cfg *config.Config) string { 11 | sarif := Sarif{ 12 | Schema: "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json", 13 | Version: "2.1.0", 14 | Runs: getRuns(report, cfg), 15 | } 16 | 17 | sarifReport, err := json.MarshalIndent(sarif, "", " ") 18 | if err != nil { 19 | log.Fatalf("failed to create Sarif report with error: %v", err) 20 | } 21 | 22 | return string(sarifReport) 23 | } 24 | 25 | func getRuns(report Report, cfg *config.Config) []Runs { 26 | return []Runs{ 27 | { 28 | Tool: getTool(cfg), 29 | Results: getResults(report), 30 | }, 31 | } 32 | } 33 | 34 | func getTool(cfg *config.Config) Tool { 35 | tool := Tool{ 36 | Driver: Driver{ 37 | Name: cfg.Name, 38 | SemanticVersion: cfg.Version, 39 | }, 40 | } 41 | 42 | return tool 43 | } 44 | 45 | func hasNoResults(report Report) bool { 46 | return len(report.Results) == 0 47 | } 48 | 49 | func messageText(secret Secret) string { 50 | return fmt.Sprintf("%s has detected secret for file %s.", secret.Description, secret.ID) 51 | } 52 | 53 | func getResults(report Report) []Results { 54 | var results []Results 55 | 56 | // if this report has no results, ensure that it is represented as [] instead of null/nil 57 | if hasNoResults(report) { 58 | results = make([]Results, 0) 59 | return results 60 | } 61 | 62 | for _, secrets := range report.Results { 63 | for _, secret := range secrets { 64 | r := Results{ 65 | Message: Message{ 66 | Text: messageText(secret), 67 | }, 68 | RuleId: secret.Description, 69 | Locations: getLocation(secret), 70 | } 71 | results = append(results, r) 72 | } 73 | } 74 | return results 75 | } 76 | 77 | func getLocation(secret Secret) []Locations { 78 | return []Locations{ 79 | { 80 | PhysicalLocation: PhysicalLocation{ 81 | ArtifactLocation: ArtifactLocation{ 82 | URI: secret.ID, 83 | }, 84 | Region: Region{ 85 | StartLine: secret.StartLine, 86 | EndLine: secret.EndLine, 87 | StartColumn: secret.StartColumn, 88 | EndColumn: secret.EndColumn, 89 | Snippet: Snippet{ 90 | Text: secret.Value, 91 | }, 92 | }, 93 | }, 94 | }, 95 | } 96 | } 97 | 98 | type Sarif struct { 99 | Schema string `json:"$schema"` 100 | Version string `json:"version"` 101 | Runs []Runs `json:"runs"` 102 | } 103 | type ShortDescription struct { 104 | Text string `json:"text"` 105 | } 106 | 107 | type Driver struct { 108 | Name string `json:"name"` 109 | SemanticVersion string `json:"semanticVersion"` 110 | } 111 | 112 | type Tool struct { 113 | Driver Driver `json:"driver"` 114 | } 115 | 116 | type Message struct { 117 | Text string `json:"text"` 118 | } 119 | 120 | type ArtifactLocation struct { 121 | URI string `json:"uri"` 122 | } 123 | 124 | type Region struct { 125 | StartLine int `json:"startLine"` 126 | StartColumn int `json:"startColumn"` 127 | EndLine int `json:"endLine"` 128 | EndColumn int `json:"endColumn"` 129 | Snippet Snippet `json:"snippet"` 130 | } 131 | 132 | type Snippet struct { 133 | Text string `json:"text"` 134 | } 135 | 136 | type PhysicalLocation struct { 137 | ArtifactLocation ArtifactLocation `json:"artifactLocation"` 138 | Region Region `json:"region"` 139 | } 140 | 141 | type Locations struct { 142 | PhysicalLocation PhysicalLocation `json:"physicalLocation"` 143 | } 144 | 145 | type Results struct { 146 | Message Message `json:"message"` 147 | RuleId string `json:"ruleId"` 148 | Locations []Locations `json:"locations"` 149 | } 150 | 151 | type Runs struct { 152 | Tool Tool `json:"tool"` 153 | Results []Results `json:"results"` 154 | } 155 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [master] 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | test: 13 | name: Test 14 | runs-on: ubuntu-latest 15 | 16 | outputs: 17 | git_tag: ${{ steps.semantic_release_info.outputs.git_tag }} 18 | version: ${{ steps.semantic_release_info.outputs.version }} 19 | notes: ${{ steps.semantic_release_info.outputs.notes }} 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v3 23 | 24 | - uses: actions/setup-go@v4 25 | with: 26 | go-version: "^1.20" 27 | - name: Go Linter 28 | run: docker run --rm -v $(pwd):/app -w /app golangci/golangci-lint:v1.52.0 golangci-lint run -v -E gofmt --timeout=5m 29 | 30 | - name: Unit Tests 31 | run: go test ./... 32 | 33 | - name: update README 34 | run: | 35 | help_message=$(go run .) 36 | echo '```' > output.txt 37 | echo "$help_message" >> output.txt 38 | echo '```' >> output.txt 39 | sed -i '//,//{ 40 | //{ 41 | p 42 | r output.txt 43 | } 44 | //!d 45 | }' README.md 46 | - name: commit and push 47 | id: git-check 48 | continue-on-error: true 49 | run: | 50 | git config --global user.email "github-actions[bot]@users.noreply.github.com" 51 | git config --global user.name "github-actions[bot]" 52 | git add README.md 53 | git diff-index --quiet HEAD || git commit -m "docs: update help message in README.md" 54 | git push 55 | 56 | - name: Gets release info 57 | id: semantic_release_info 58 | if: github.event_name == 'workflow_dispatch' 59 | uses: jossef/action-semantic-release-info@v2.1.0 60 | env: 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | 63 | build: 64 | name: Build and Release 65 | runs-on: ubuntu-latest 66 | needs: test 67 | if: ${{ needs.test.outputs.git_tag }} 68 | steps: 69 | - name: Checkout code 70 | uses: actions/checkout@v3 71 | 72 | - uses: actions/setup-go@v4 73 | with: 74 | go-version: "^1.20" 75 | 76 | - name: Go Mod Tidy 77 | run: go mod tidy 78 | 79 | - name: Go Build 80 | env: 81 | VERSION: ${{ needs.test.outputs.version }} 82 | CGO_ENABLED: 0 83 | GOOS: linux 84 | GOARCH: amd64 85 | run: go build -ldflags "-s -w -X github.com/checkmarx/2ms/cmd.Version=$VERSION" -a -installsuffix cgo -o bin/2ms main.go 86 | 87 | - name: Set up Docker Buildx 88 | uses: docker/setup-buildx-action@v2 89 | 90 | - name: Login to DockerHub 91 | uses: docker/login-action@v2 92 | with: 93 | username: ${{ secrets.DOCKERHUB_USERNAME }} 94 | password: ${{ secrets.DOCKERHUB_TOKEN }} 95 | 96 | - name: Creating Release 97 | uses: softprops/action-gh-release@v0.1.15 98 | with: 99 | tag_name: ${{ needs.test.outputs.git_tag }} 100 | name: ${{ needs.test.outputs.git_tag }} 101 | body: ${{ needs.test.outputs.notes }} 102 | target_commitish: ${{ steps.commit_and_push.outputs.latest_commit_hash }} 103 | files: | 104 | bin/2ms 105 | 106 | - name: Build and push 107 | uses: docker/build-push-action@v4 108 | with: 109 | context: . 110 | push: true 111 | tags: | 112 | checkmarx/2ms:latest 113 | checkmarx/2ms:${{ needs.test.outputs.version }} 114 | 115 | - name: Update Docker repo description 116 | uses: peter-evans/dockerhub-description@v3 117 | with: 118 | username: ${{ secrets.DOCKERHUB_USERNAME }} 119 | password: ${{ secrets.DOCKERHUB_TOKEN }} 120 | repository: checkmarx/2ms 121 | -------------------------------------------------------------------------------- /plugins/slack_test.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "testing" 8 | "time" 9 | 10 | "github.com/slack-go/slack" 11 | ) 12 | 13 | type mockSlackClient struct { 14 | channels []slack.Channel 15 | err error 16 | } 17 | 18 | func (m *mockSlackClient) GetConversations(params *slack.GetConversationsParameters) ([]slack.Channel, string, error) { 19 | return m.channels, "", m.err 20 | } 21 | func (m *mockSlackClient) ListTeams(params slack.ListTeamsParameters) ([]slack.Team, string, error) { 22 | return nil, "", errors.New("not implemented") 23 | } 24 | 25 | func TestGetChannels(t *testing.T) { 26 | 27 | tests := []struct { 28 | name string 29 | slackApi mockSlackClient 30 | teamId string 31 | wantedChannels []string 32 | expectedResult []slack.Channel 33 | expectedError error 34 | }{ 35 | { 36 | name: "get all channels", 37 | slackApi: mockSlackClient{ 38 | channels: []slack.Channel{ 39 | {GroupConversation: slack.GroupConversation{Name: "channel1", Conversation: slack.Conversation{ID: "C123456"}}}, 40 | {GroupConversation: slack.GroupConversation{Name: "channel2", Conversation: slack.Conversation{ID: "C234567"}}}, 41 | }, 42 | }, 43 | teamId: "T123456", 44 | wantedChannels: []string{}, 45 | expectedResult: []slack.Channel{ 46 | {GroupConversation: slack.GroupConversation{Name: "channel1", Conversation: slack.Conversation{ID: "C123456"}}}, 47 | {GroupConversation: slack.GroupConversation{Name: "channel2", Conversation: slack.Conversation{ID: "C234567"}}}, 48 | }, 49 | expectedError: nil, 50 | }, 51 | { 52 | name: "get specific channels", 53 | slackApi: mockSlackClient{ 54 | channels: []slack.Channel{ 55 | {GroupConversation: slack.GroupConversation{Name: "channel1", Conversation: slack.Conversation{ID: "C123456"}}}, 56 | {GroupConversation: slack.GroupConversation{Name: "channel2", Conversation: slack.Conversation{ID: "C234567"}}}, 57 | }, 58 | }, 59 | teamId: "T123456", 60 | wantedChannels: []string{"channel1", "C234567"}, 61 | expectedResult: []slack.Channel{ 62 | {GroupConversation: slack.GroupConversation{Name: "channel1", Conversation: slack.Conversation{ID: "C123456"}}}, 63 | {GroupConversation: slack.GroupConversation{Name: "channel2", Conversation: slack.Conversation{ID: "C234567"}}}, 64 | }, 65 | expectedError: nil, 66 | }, 67 | { 68 | name: "get specific channels not found", 69 | slackApi: mockSlackClient{ 70 | channels: []slack.Channel{ 71 | {GroupConversation: slack.GroupConversation{Name: "channel1", Conversation: slack.Conversation{ID: "C123456"}}}, 72 | {GroupConversation: slack.GroupConversation{Name: "channel2", Conversation: slack.Conversation{ID: "C234567"}}}, 73 | }, 74 | }, 75 | teamId: "T123456", 76 | wantedChannels: []string{"channel3", "C345678"}, 77 | expectedResult: []slack.Channel{}, 78 | expectedError: nil, 79 | }, 80 | { 81 | name: "get channels error", 82 | slackApi: mockSlackClient{ 83 | err: fmt.Errorf("some error"), 84 | channels: []slack.Channel{}, 85 | }, 86 | teamId: "T123456", 87 | wantedChannels: []string{}, 88 | expectedResult: []slack.Channel{}, 89 | expectedError: fmt.Errorf("error while getting channels: %w", errors.New("some error")), 90 | }, 91 | } 92 | 93 | for _, tt := range tests { 94 | t.Run(tt.name, func(t *testing.T) { 95 | result, err := getChannels(&tt.slackApi, tt.teamId, tt.wantedChannels) 96 | if err != nil && tt.expectedError == nil { 97 | t.Errorf("unexpected error: %v", err) 98 | } 99 | if err == nil && tt.expectedError != nil { 100 | t.Errorf("expected error: %v, but got nil", tt.expectedError) 101 | } 102 | if err != nil && tt.expectedError != nil { 103 | return 104 | } 105 | if len(*result) != len(tt.expectedResult) { 106 | t.Errorf("expected %d channels, but got %d", len(tt.expectedResult), len(*result)) 107 | } 108 | for i, c := range *result { 109 | if c.Name != tt.expectedResult[i].Name || c.ID != tt.expectedResult[i].ID { 110 | t.Errorf("expected channel %v, but got %v", tt.expectedResult[i], c) 111 | } 112 | } 113 | }) 114 | } 115 | } 116 | 117 | func formatSecondsAnd6DigitsMiliseconds(t time.Time) string { 118 | n := float64(t.UnixMicro()) / float64(time.Millisecond) 119 | return strconv.FormatFloat(n, 'f', 6, 64) 120 | } 121 | 122 | const ( 123 | noLimit = 0 124 | ) 125 | 126 | func TestIsMessageOutOfRange(t *testing.T) { 127 | tests := []struct { 128 | name string 129 | message slack.Message 130 | backwardDuration time.Duration 131 | currentMessagesCount int 132 | limitMessagesCount int 133 | expectedOutOfRange bool 134 | }{ 135 | { 136 | name: "message is within range", 137 | message: slack.Message{ 138 | Msg: slack.Msg{ 139 | Timestamp: formatSecondsAnd6DigitsMiliseconds(timeNow), 140 | }, 141 | }, 142 | backwardDuration: time.Minute, 143 | currentMessagesCount: 0, 144 | limitMessagesCount: noLimit, 145 | expectedOutOfRange: false, 146 | }, 147 | { 148 | name: "message is out of range due to backward duration", 149 | message: slack.Message{ 150 | Msg: slack.Msg{ 151 | Timestamp: formatSecondsAnd6DigitsMiliseconds(timeNow.Add(-time.Minute * 2)), 152 | }, 153 | }, 154 | backwardDuration: time.Minute, 155 | currentMessagesCount: 0, 156 | limitMessagesCount: noLimit, 157 | expectedOutOfRange: true, 158 | }, 159 | { 160 | name: "message is out of range due to message count limit", 161 | message: slack.Message{ 162 | Msg: slack.Msg{ 163 | Timestamp: formatSecondsAnd6DigitsMiliseconds(timeNow), 164 | }, 165 | }, 166 | backwardDuration: noLimit, 167 | currentMessagesCount: 1, 168 | limitMessagesCount: 1, 169 | expectedOutOfRange: true, 170 | }, 171 | } 172 | 173 | for _, tt := range tests { 174 | t.Run(tt.name, func(t *testing.T) { 175 | outOfRange, err := isMessageOutOfRange(tt.message, tt.backwardDuration, tt.currentMessagesCount, tt.limitMessagesCount) 176 | if err != nil { 177 | t.Errorf("unexpected error: %v", err) 178 | } 179 | if outOfRange != tt.expectedOutOfRange { 180 | t.Errorf("expected outOfRange to be %v, but got %v", tt.expectedOutOfRange, outOfRange) 181 | } 182 | }) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /plugins/slack.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/rs/zerolog/log" 9 | "github.com/slack-go/slack" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | const ( 14 | slackTokenFlag = "token" 15 | slackTeamFlag = "team" 16 | slackChannelFlag = "channel" 17 | slackBackwardDurationFlag = "duration" 18 | slackMessagesCountFlag = "messages-count" 19 | ) 20 | 21 | const slackDefaultDateFrom = time.Hour * 24 * 14 22 | 23 | type SlackPlugin struct { 24 | Plugin 25 | Channels 26 | Token string 27 | } 28 | 29 | func (p *SlackPlugin) GetName() string { 30 | return "slack" 31 | } 32 | 33 | var ( 34 | tokenArg string 35 | teamArg string 36 | channelsArg []string 37 | backwardDurationArg time.Duration 38 | messagesCountArg int 39 | ) 40 | 41 | func (p *SlackPlugin) DefineCommand(channels Channels) (*cobra.Command, error) { 42 | p.Channels = channels 43 | 44 | command := &cobra.Command{ 45 | Use: fmt.Sprintf("%s --%s TOKEN --%s TEAM", p.GetName(), slackTokenFlag, slackTeamFlag), 46 | Short: "Scan Slack team", 47 | Long: "Scan Slack team for sensitive information.", 48 | Run: func(cmd *cobra.Command, args []string) { 49 | p.getItems() 50 | }, 51 | } 52 | 53 | command.Flags().StringVar(&tokenArg, slackTokenFlag, "", "Slack token [required]") 54 | err := command.MarkFlagRequired(slackTokenFlag) 55 | if err != nil { 56 | return nil, fmt.Errorf("error while marking flag %s as required: %w", slackTokenFlag, err) 57 | } 58 | command.Flags().StringVar(&teamArg, slackTeamFlag, "", "Slack team name or ID [required]") 59 | err = command.MarkFlagRequired(slackTeamFlag) 60 | if err != nil { 61 | return nil, fmt.Errorf("error while marking flag %s as required: %w", slackTeamFlag, err) 62 | } 63 | command.Flags().StringSliceVar(&channelsArg, slackChannelFlag, []string{}, "Slack channels to scan") 64 | command.Flags().DurationVar(&backwardDurationArg, slackBackwardDurationFlag, slackDefaultDateFrom, "Slack backward duration for messages (ex: 24h, 7d, 1M, 1y)") 65 | command.Flags().IntVar(&messagesCountArg, slackMessagesCountFlag, 0, "Slack messages count to scan (0 = all messages)") 66 | 67 | return command, nil 68 | } 69 | 70 | func (p *SlackPlugin) getItems() { 71 | slackApi := slack.New(tokenArg) 72 | 73 | team, err := getTeam(slackApi, teamArg) 74 | if err != nil { 75 | p.Errors <- fmt.Errorf("error while getting team: %w", err) 76 | return 77 | } 78 | 79 | channels, err := getChannels(slackApi, team.ID, channelsArg) 80 | if err != nil { 81 | p.Errors <- fmt.Errorf("error while getting channels for team %s: %w", team.Name, err) 82 | return 83 | } 84 | if len(*channels) == 0 { 85 | log.Warn().Msgf("No channels found for team %s", team.Name) 86 | return 87 | } 88 | 89 | log.Info().Msgf("Found %d channels for team %s", len(*channels), team.Name) 90 | p.WaitGroup.Add(len(*channels)) 91 | for _, channel := range *channels { 92 | go p.getItemsFromChannel(slackApi, channel) 93 | } 94 | } 95 | 96 | func (p *SlackPlugin) getItemsFromChannel(slackApi *slack.Client, channel slack.Channel) { 97 | defer p.WaitGroup.Done() 98 | log.Info().Msgf("Getting items from channel %s", channel.Name) 99 | 100 | cursor := "" 101 | counter := 0 102 | for { 103 | history, err := slackApi.GetConversationHistory(&slack.GetConversationHistoryParameters{ 104 | Cursor: cursor, 105 | ChannelID: channel.ID, 106 | }) 107 | if err != nil { 108 | p.Errors <- fmt.Errorf("error while getting history for channel %s: %w", channel.Name, err) 109 | return 110 | } 111 | for _, message := range history.Messages { 112 | outOfRange, err := isMessageOutOfRange(message, backwardDurationArg, counter, messagesCountArg) 113 | if err != nil { 114 | p.Errors <- fmt.Errorf("error while checking message: %w", err) 115 | return 116 | } 117 | if outOfRange { 118 | break 119 | } 120 | if message.Text != "" { 121 | url, err := slackApi.GetPermalink(&slack.PermalinkParameters{Channel: channel.ID, Ts: message.Timestamp}) 122 | if err != nil { 123 | log.Warn().Msgf("Error while getting permalink for message %s: %s", message.Timestamp, err) 124 | url = fmt.Sprintf("Channel: %s; Message: %s", channel.Name, message.Timestamp) 125 | } 126 | p.Items <- Item{ 127 | Content: message.Text, 128 | ID: url, 129 | } 130 | } 131 | counter++ 132 | } 133 | if history.ResponseMetaData.NextCursor == "" { 134 | break 135 | } 136 | cursor = history.ResponseMetaData.NextCursor 137 | } 138 | } 139 | 140 | // Declare it to be consistent with all comparaisons 141 | var timeNow = time.Now() 142 | 143 | func isMessageOutOfRange(message slack.Message, backwardDuration time.Duration, currentMessagesCount int, limitMessagesCount int) (bool, error) { 144 | if backwardDuration != 0 { 145 | timestamp, err := strconv.ParseFloat(message.Timestamp, 64) 146 | if err != nil { 147 | return true, fmt.Errorf("error while parsing timestamp: %w", err) 148 | } 149 | messageDate := time.Unix(int64(timestamp), 0) 150 | if messageDate.Before(timeNow.Add(-backwardDuration)) { 151 | return true, nil 152 | } 153 | } 154 | if limitMessagesCount != 0 && currentMessagesCount >= limitMessagesCount { 155 | return true, nil 156 | } 157 | return false, nil 158 | } 159 | 160 | type ISlackClient interface { 161 | GetConversations(*slack.GetConversationsParameters) ([]slack.Channel, string, error) 162 | ListTeams(slack.ListTeamsParameters) ([]slack.Team, string, error) 163 | } 164 | 165 | func getTeam(slackApi ISlackClient, teamName string) (*slack.Team, error) { 166 | cursorHolder := "" 167 | for { 168 | teams, cursor, err := slackApi.ListTeams(slack.ListTeamsParameters{Cursor: cursorHolder}) 169 | if err != nil { 170 | return nil, fmt.Errorf("error while getting teams: %w", err) 171 | } 172 | for _, team := range teams { 173 | if team.Name == teamName || team.ID == teamName { 174 | return &team, nil 175 | } 176 | } 177 | if cursor == "" { 178 | break 179 | } 180 | cursorHolder = cursor 181 | } 182 | return nil, fmt.Errorf("team '%s' not found", teamName) 183 | } 184 | 185 | func getChannels(slackApi ISlackClient, teamId string, wantedChannels []string) (*[]slack.Channel, error) { 186 | cursorHolder := "" 187 | selectedChannels := []slack.Channel{} 188 | for { 189 | channels, cursor, err := slackApi.GetConversations(&slack.GetConversationsParameters{ 190 | Cursor: cursorHolder, 191 | TeamID: teamId, 192 | }) 193 | if err != nil { 194 | return nil, fmt.Errorf("error while getting channels: %w", err) 195 | } 196 | if len(wantedChannels) == 0 { 197 | selectedChannels = append(selectedChannels, channels...) 198 | } else { 199 | for _, channel := range wantedChannels { 200 | for _, c := range channels { 201 | if c.Name == channel || c.ID == channel { 202 | selectedChannels = append(selectedChannels, c) 203 | } 204 | } 205 | } 206 | if len(selectedChannels) == len(wantedChannels) { 207 | return &selectedChannels, nil 208 | } 209 | } 210 | if cursor == "" { 211 | return &selectedChannels, nil 212 | } 213 | cursorHolder = cursor 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/checkmarx/2ms/config" 10 | "github.com/checkmarx/2ms/lib" 11 | 12 | "sync" 13 | "time" 14 | 15 | "github.com/checkmarx/2ms/plugins" 16 | "github.com/checkmarx/2ms/reporting" 17 | "github.com/checkmarx/2ms/secrets" 18 | 19 | "github.com/rs/zerolog" 20 | "github.com/rs/zerolog/log" 21 | "github.com/spf13/cobra" 22 | "github.com/spf13/viper" 23 | ) 24 | 25 | var Version = "0.0.0" 26 | 27 | const ( 28 | timeSleepInterval = 50 29 | jsonFormat = "json" 30 | yamlFormat = "yaml" 31 | sarifFormat = "sarif" 32 | configFileFlag = "config" 33 | 34 | tagsFlagName = "tags" 35 | logLevelFlagName = "log-level" 36 | reportPathFlagName = "report-path" 37 | stdoutFormatFlagName = "stdout-format" 38 | customRegexRuleFlagName = "regex" 39 | ) 40 | 41 | var ( 42 | tagsVar []string 43 | logLevelVar string 44 | reportPathVar []string 45 | stdoutFormatVar string 46 | customRegexRuleVar []string 47 | ) 48 | 49 | var rootCmd = &cobra.Command{ 50 | Use: "2ms", 51 | Short: "2ms Secrets Detection", 52 | Long: "2ms Secrets Detection: A tool to detect secrets in public websites and communication services.", 53 | Version: Version, 54 | } 55 | 56 | const envPrefix = "2MS" 57 | 58 | var configFilePath string 59 | var vConfig = viper.New() 60 | 61 | var allPlugins = []plugins.IPlugin{ 62 | &plugins.ConfluencePlugin{}, 63 | &plugins.DiscordPlugin{}, 64 | &plugins.FileSystemPlugin{}, 65 | &plugins.SlackPlugin{}, 66 | &plugins.PaligoPlugin{}, 67 | &plugins.GitPlugin{}, 68 | } 69 | 70 | var channels = plugins.Channels{ 71 | Items: make(chan plugins.Item), 72 | Errors: make(chan error), 73 | WaitGroup: &sync.WaitGroup{}, 74 | } 75 | 76 | var report = reporting.Init() 77 | var secretsChan = make(chan reporting.Secret) 78 | 79 | // TODO: docs 80 | func initialize() { 81 | zerolog.SetGlobalLevel(zerolog.InfoLevel) 82 | 83 | configFilePath, err := rootCmd.Flags().GetString(configFileFlag) 84 | if err != nil { 85 | cobra.CheckErr(err) 86 | } 87 | cobra.CheckErr(lib.LoadConfig(vConfig, configFilePath)) 88 | cobra.CheckErr(lib.BindFlags(rootCmd, vConfig, envPrefix)) 89 | 90 | switch strings.ToLower(logLevelVar) { 91 | case "trace": 92 | zerolog.SetGlobalLevel(zerolog.TraceLevel) 93 | case "debug": 94 | zerolog.SetGlobalLevel(zerolog.DebugLevel) 95 | case "info": 96 | zerolog.SetGlobalLevel(zerolog.InfoLevel) 97 | case "warn": 98 | zerolog.SetGlobalLevel(zerolog.WarnLevel) 99 | case "err", "error": 100 | zerolog.SetGlobalLevel(zerolog.ErrorLevel) 101 | case "fatal": 102 | zerolog.SetGlobalLevel(zerolog.FatalLevel) 103 | default: 104 | zerolog.SetGlobalLevel(zerolog.InfoLevel) 105 | } 106 | } 107 | 108 | func Execute() { 109 | vConfig.SetEnvPrefix(envPrefix) 110 | vConfig.AutomaticEnv() 111 | 112 | cobra.OnInitialize(initialize) 113 | rootCmd.PersistentFlags().StringVar(&configFilePath, configFileFlag, "", "config file path") 114 | cobra.CheckErr(rootCmd.MarkPersistentFlagFilename(configFileFlag, "yaml", "yml", "json")) 115 | rootCmd.PersistentFlags().StringSliceVar(&tagsVar, tagsFlagName, []string{"all"}, "select rules to be applied") 116 | rootCmd.PersistentFlags().StringVar(&logLevelVar, logLevelFlagName, "info", "log level (trace, debug, info, warn, error, fatal)") 117 | rootCmd.PersistentFlags().StringSliceVar(&reportPathVar, reportPathFlagName, []string{}, "path to generate report files. The output format will be determined by the file extension (.json, .yaml, .sarif)") 118 | rootCmd.PersistentFlags().StringVar(&stdoutFormatVar, stdoutFormatFlagName, "yaml", "stdout output format, available formats are: json, yaml, sarif") 119 | rootCmd.PersistentFlags().StringArrayVar(&customRegexRuleVar, customRegexRuleFlagName, []string{}, "custom regexes to apply to the scan, must be valid Go regex") 120 | 121 | rootCmd.PersistentPreRun = preRun 122 | rootCmd.PersistentPostRun = postRun 123 | 124 | group := "Commands" 125 | rootCmd.AddGroup(&cobra.Group{Title: group, ID: group}) 126 | 127 | for _, plugin := range allPlugins { 128 | subCommand, err := plugin.DefineCommand(channels) 129 | subCommand.GroupID = group 130 | if err != nil { 131 | log.Fatal().Msg(fmt.Sprintf("error while defining command for plugin %s: %s", plugin.GetName(), err.Error())) 132 | } 133 | rootCmd.AddCommand(subCommand) 134 | } 135 | 136 | if err := rootCmd.Execute(); err != nil { 137 | log.Fatal().Msg(err.Error()) 138 | } 139 | } 140 | 141 | func validateTags(tags []string) { 142 | for _, tag := range tags { 143 | if !(strings.EqualFold(tag, "all") || strings.EqualFold(tag, secrets.TagApiKey) || strings.EqualFold(tag, secrets.TagClientId) || 144 | strings.EqualFold(tag, secrets.TagClientSecret) || strings.EqualFold(tag, secrets.TagSecretKey) || strings.EqualFold(tag, secrets.TagAccessKey) || 145 | strings.EqualFold(tag, secrets.TagAccessId) || strings.EqualFold(tag, secrets.TagApiToken) || strings.EqualFold(tag, secrets.TagAccessToken) || 146 | strings.EqualFold(tag, secrets.TagRefreshToken) || strings.EqualFold(tag, secrets.TagPrivateKey) || strings.EqualFold(tag, secrets.TagPublicKey) || 147 | strings.EqualFold(tag, secrets.TagEncryptionKey) || strings.EqualFold(tag, secrets.TagTriggerToken) || strings.EqualFold(tag, secrets.TagRegistrationToken) || 148 | strings.EqualFold(tag, secrets.TagPassword) || strings.EqualFold(tag, secrets.TagUploadToken) || strings.EqualFold(tag, secrets.TagPublicSecret) || 149 | strings.EqualFold(tag, secrets.TagSensitiveUrl) || strings.EqualFold(tag, secrets.TagWebhook)) { 150 | log.Fatal().Msgf(`invalid filter: %s`, tag) 151 | } 152 | } 153 | } 154 | 155 | func validateFormat(stdout string, reportPath []string) { 156 | if !(strings.EqualFold(stdout, yamlFormat) || strings.EqualFold(stdout, jsonFormat) || strings.EqualFold(stdout, sarifFormat)) { 157 | log.Fatal().Msgf(`invalid output format: %s, available formats are: json, yaml and sarif`, stdout) 158 | } 159 | for _, path := range reportPath { 160 | 161 | fileExtension := filepath.Ext(path) 162 | format := strings.TrimPrefix(fileExtension, ".") 163 | if !(strings.EqualFold(format, yamlFormat) || strings.EqualFold(format, jsonFormat) || strings.EqualFold(format, sarifFormat)) { 164 | log.Fatal().Msgf(`invalid report extension: %s, available extensions are: json, yaml and sarif`, format) 165 | } 166 | } 167 | } 168 | 169 | func preRun(cmd *cobra.Command, args []string) { 170 | validateTags(tagsVar) 171 | 172 | secrets := secrets.Init(tagsVar) 173 | 174 | if err := secrets.AddRegexRules(customRegexRuleVar); err != nil { 175 | log.Fatal().Msg(err.Error()) 176 | } 177 | 178 | go func() { 179 | for { 180 | select { 181 | case item := <-channels.Items: 182 | report.TotalItemsScanned++ 183 | channels.WaitGroup.Add(1) 184 | go secrets.Detect(secretsChan, item, channels.WaitGroup) 185 | case secret := <-secretsChan: 186 | report.TotalSecretsFound++ 187 | report.Results[secret.ID] = append(report.Results[secret.ID], secret) 188 | case err, ok := <-channels.Errors: 189 | if !ok { 190 | return 191 | } 192 | log.Fatal().Msg(err.Error()) 193 | } 194 | } 195 | }() 196 | } 197 | 198 | func postRun(cmd *cobra.Command, args []string) { 199 | channels.WaitGroup.Wait() 200 | 201 | validateFormat(stdoutFormatVar, reportPathVar) 202 | 203 | cfg := config.LoadConfig("2ms", Version) 204 | 205 | // Wait for last secret to be added to report 206 | time.Sleep(time.Millisecond * timeSleepInterval) 207 | 208 | // ------------------------------------- 209 | // Show Report 210 | if report.TotalItemsScanned > 0 { 211 | report.ShowReport(stdoutFormatVar, cfg) 212 | if len(reportPathFlagName) > 0 { 213 | err := report.WriteFile(reportPathVar, cfg) 214 | if err != nil { 215 | log.Error().Msgf("Failed to create report file with error: %s", err) 216 | } 217 | } 218 | } else { 219 | log.Error().Msg("Scan completed with empty content") 220 | os.Exit(0) 221 | } 222 | 223 | if report.TotalSecretsFound > 0 { 224 | os.Exit(1) 225 | } else { 226 | os.Exit(0) 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /plugins/discord.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | 8 | "github.com/bwmarrin/discordgo" 9 | "github.com/rs/zerolog" 10 | "github.com/rs/zerolog/log" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | const ( 15 | tokenFlag = "token" 16 | serversFlag = "server" 17 | channelsFlag = "channel" 18 | fromDateFlag = "duration" 19 | messagesCountFlag = "messages-count" 20 | ) 21 | 22 | const defaultDateFrom = time.Hour * 24 * 14 23 | 24 | type DiscordPlugin struct { 25 | Token string 26 | Guilds []string 27 | Channels []string 28 | Count int 29 | BackwardDuration time.Duration 30 | Session *discordgo.Session 31 | 32 | errChan chan error 33 | itemChan chan Item 34 | waitGroup *sync.WaitGroup 35 | } 36 | 37 | func (p *DiscordPlugin) GetName() string { 38 | return "discord" 39 | } 40 | 41 | func (p *DiscordPlugin) DefineCommand(channels Channels) (*cobra.Command, error) { 42 | var discordCmd = &cobra.Command{ 43 | Use: fmt.Sprintf("%s --%s TOKEN --%s SERVER", p.GetName(), tokenFlag, serversFlag), 44 | Short: "Scan Discord server", 45 | Long: "Scan Discord server for sensitive information.", 46 | } 47 | flags := discordCmd.Flags() 48 | 49 | flags.StringVar(&p.Token, tokenFlag, "", "Discord token [required]") 50 | err := discordCmd.MarkFlagRequired(tokenFlag) 51 | if err != nil { 52 | return nil, fmt.Errorf("error while marking '%s' flag as required: %w", tokenFlag, err) 53 | } 54 | flags.StringSliceVar(&p.Guilds, serversFlag, []string{}, "Discord servers IDs to scan [required]") 55 | err = discordCmd.MarkFlagRequired(serversFlag) 56 | if err != nil { 57 | return nil, fmt.Errorf("error while marking '%s' flag as required: %w", serversFlag, err) 58 | } 59 | flags.StringSliceVar(&p.Channels, channelsFlag, []string{}, "Discord channels IDs to scan. If not provided, all channels will be scanned") 60 | flags.DurationVar(&p.BackwardDuration, fromDateFlag, defaultDateFrom, "The time interval to scan from the current time. For example, 24h for 24 hours or 7d for 7 days.") 61 | flags.IntVar(&p.Count, messagesCountFlag, 0, "The number of messages to scan. If not provided, all messages will be scanned until the fromDate flag value.") 62 | 63 | discordCmd.Run = func(cmd *cobra.Command, args []string) { 64 | err := p.initialize(cmd) 65 | if err != nil { 66 | channels.Errors <- fmt.Errorf("discord plugin initialization failed: %w", err) 67 | return 68 | } 69 | 70 | p.getItems(channels.Items, channels.Errors, channels.WaitGroup) 71 | } 72 | 73 | return discordCmd, nil 74 | } 75 | 76 | func (p *DiscordPlugin) initialize(cmd *cobra.Command) error { 77 | if len(p.Channels) == 0 { 78 | log.Warn().Msg("discord channels not provided. Will scan all channels") 79 | } 80 | 81 | if p.Count == 0 && p.BackwardDuration == 0 { 82 | return fmt.Errorf("discord messages count or from date arg is missing. Plugin initialization failed") 83 | } 84 | 85 | return nil 86 | } 87 | 88 | func (p *DiscordPlugin) getItems(itemsChan chan Item, errChan chan error, wg *sync.WaitGroup) { 89 | defer wg.Done() 90 | 91 | p.errChan = errChan 92 | p.itemChan = itemsChan 93 | p.waitGroup = wg 94 | 95 | err := p.getDiscordReady() 96 | if err != nil { 97 | errChan <- err 98 | return 99 | } 100 | 101 | guilds := p.getGuildsByNameOrIDs() 102 | log.Info().Msgf("Found %d guilds", len(guilds)) 103 | 104 | wg.Add(len(guilds)) 105 | for _, guild := range guilds { 106 | go p.readGuildMessages(guild) 107 | } 108 | } 109 | 110 | func (p *DiscordPlugin) getDiscordReady() (err error) { 111 | p.Session, err = discordgo.New(p.Token) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | p.Session.StateEnabled = true 117 | ready := make(chan error) 118 | p.Session.AddHandlerOnce(func(s *discordgo.Session, r *discordgo.Ready) { 119 | ready <- nil 120 | }) 121 | go func() { 122 | err := p.Session.Open() 123 | if err != nil { 124 | ready <- err 125 | } 126 | }() 127 | time.AfterFunc(time.Second*10, func() { 128 | ready <- fmt.Errorf("discord session timeout") 129 | }) 130 | 131 | err = <-ready 132 | if err != nil { 133 | return err 134 | } 135 | 136 | return nil 137 | } 138 | 139 | func (p *DiscordPlugin) getGuildsByNameOrIDs() []*discordgo.Guild { 140 | var result []*discordgo.Guild 141 | 142 | for _, guild := range p.Guilds { 143 | for _, g := range p.Session.State.Guilds { 144 | if g.Name == guild || g.ID == guild { 145 | result = append(result, g) 146 | } 147 | } 148 | } 149 | 150 | return result 151 | } 152 | 153 | func (p *DiscordPlugin) readGuildMessages(guild *discordgo.Guild) { 154 | defer p.waitGroup.Done() 155 | 156 | guildLogger := log.With().Str("guild", guild.Name).Logger() 157 | guildLogger.Debug().Send() 158 | 159 | selectedChannels := p.getChannelsByNameOrIDs(guild) 160 | guildLogger.Info().Msgf("Found %d channels", len(selectedChannels)) 161 | 162 | p.waitGroup.Add(len(selectedChannels)) 163 | for _, channel := range selectedChannels { 164 | go p.readChannelMessages(channel) 165 | } 166 | } 167 | 168 | func (p *DiscordPlugin) getChannelsByNameOrIDs(guild *discordgo.Guild) []*discordgo.Channel { 169 | var result []*discordgo.Channel 170 | if len(p.Channels) == 0 { 171 | return guild.Channels 172 | } 173 | 174 | for _, channel := range p.Channels { 175 | for _, c := range guild.Channels { 176 | if c.Name == channel || c.ID == channel { 177 | result = append(result, c) 178 | } 179 | } 180 | } 181 | 182 | return result 183 | } 184 | 185 | func (p *DiscordPlugin) readChannelMessages(channel *discordgo.Channel) { 186 | defer p.waitGroup.Done() 187 | 188 | channelLogger := log.With().Str("guildID", channel.GuildID).Str("channel", channel.Name).Logger() 189 | channelLogger.Debug().Send() 190 | 191 | permission, err := p.Session.UserChannelPermissions(p.Session.State.User.ID, channel.ID) 192 | if err != nil { 193 | if err, ok := err.(*discordgo.RESTError); ok { 194 | if err.Message.Code == 50001 { 195 | channelLogger.Debug().Msg("No read permissions") 196 | return 197 | } 198 | } 199 | 200 | channelLogger.Error().Err(err).Msg("Failed to get permissions") 201 | p.errChan <- err 202 | return 203 | } 204 | if permission&discordgo.PermissionViewChannel == 0 { 205 | channelLogger.Debug().Msg("No read permissions") 206 | return 207 | } 208 | if channel.Type != discordgo.ChannelTypeGuildText { 209 | channelLogger.Debug().Msg("Not a text channel") 210 | return 211 | } 212 | 213 | messages, err := p.getMessages(channel.ID, channelLogger) 214 | if err != nil { 215 | channelLogger.Error().Err(err).Msg("Failed to get messages") 216 | p.errChan <- err 217 | return 218 | } 219 | channelLogger.Info().Msgf("Found %d messages", len(messages)) 220 | 221 | items := convertMessagesToItems(channel.GuildID, &messages) 222 | for _, item := range *items { 223 | p.itemChan <- item 224 | } 225 | } 226 | 227 | func (p *DiscordPlugin) getMessages(channelID string, logger zerolog.Logger) ([]*discordgo.Message, error) { 228 | var messages []*discordgo.Message 229 | threadMessages := []*discordgo.Message{} 230 | 231 | var beforeID string 232 | 233 | m, err := p.Session.ChannelMessages(channelID, 100, beforeID, "", "") 234 | if err != nil { 235 | return nil, err 236 | } 237 | 238 | lastMessage := false 239 | for len(m) > 0 && !lastMessage { 240 | 241 | for _, message := range m { 242 | 243 | timeSince := time.Since(message.Timestamp) 244 | if p.BackwardDuration > 0 && timeSince > p.BackwardDuration { 245 | logger.Debug().Msgf("Reached time limit (%s). Last message is %s old", p.BackwardDuration.String(), timeSince.Round(time.Hour).String()) 246 | lastMessage = true 247 | break 248 | } 249 | 250 | if p.Count > 0 && len(messages) == p.Count { 251 | logger.Debug().Msgf("Reached message count (%d)", p.Count) 252 | lastMessage = true 253 | break 254 | } 255 | 256 | if message.Thread != nil { 257 | logger.Info().Msgf("Found thread %s", message.Thread.Name) 258 | tMgs, err := p.getMessages(message.Thread.ID, logger.With().Str("thread", message.Thread.Name).Logger()) 259 | if err != nil { 260 | return nil, err 261 | } 262 | threadMessages = append(threadMessages, tMgs...) 263 | } 264 | 265 | messages = append(messages, message) 266 | beforeID = message.ID 267 | } 268 | 269 | m, err = p.Session.ChannelMessages(channelID, 100, beforeID, "", "") 270 | if err != nil { 271 | return nil, err 272 | } 273 | } 274 | 275 | return append(messages, threadMessages...), nil 276 | } 277 | 278 | func convertMessagesToItems(guildId string, messages *[]*discordgo.Message) *[]Item { 279 | items := []Item{} 280 | for _, message := range *messages { 281 | items = append(items, Item{ 282 | Content: message.Content, 283 | ID: fmt.Sprintf("https://discord.com/channels/%s/%s/%s", guildId, message.ChannelID, message.ID), 284 | }) 285 | } 286 | return &items 287 | } 288 | -------------------------------------------------------------------------------- /plugins/confluence.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | "sync" 9 | 10 | "github.com/checkmarx/2ms/lib" 11 | "github.com/rs/zerolog/log" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | const ( 16 | argUrl = "url" 17 | argSpaces = "spaces" 18 | argUsername = "username" 19 | argToken = "token" 20 | argHistory = "history" 21 | confluenceDefaultWindow = 25 22 | confluenceMaxRequests = 500 23 | ) 24 | 25 | type ConfluencePlugin struct { 26 | Plugin 27 | URL string 28 | Token string 29 | Username string 30 | Spaces []string 31 | History bool 32 | } 33 | 34 | func (p *ConfluencePlugin) GetName() string { 35 | return "confluence" 36 | } 37 | 38 | func (p *ConfluencePlugin) GetCredentials() (string, string) { 39 | return p.Username, p.Token 40 | } 41 | 42 | func (p *ConfluencePlugin) GetAuthorizationHeader() string { 43 | return lib.CreateBasicAuthCredentials(p) 44 | } 45 | 46 | func (p *ConfluencePlugin) DefineCommand(channels Channels) (*cobra.Command, error) { 47 | var confluenceCmd = &cobra.Command{ 48 | Use: fmt.Sprintf("%s --%s URL", p.GetName(), argUrl), 49 | Short: "Scan Confluence server", 50 | Long: "Scan Confluence server for sensitive information", 51 | } 52 | 53 | flags := confluenceCmd.Flags() 54 | flags.StringVar(&p.URL, argUrl, "", "Confluence server URL (example: https://company.atlassian.net/wiki) [required]") 55 | flags.StringSliceVar(&p.Spaces, argSpaces, []string{}, "Confluence spaces: The names or IDs of the spaces to scan") 56 | flags.StringVar(&p.Username, argUsername, "", "Confluence user name or email for authentication") 57 | flags.StringVar(&p.Token, argToken, "", "The Confluence API token for authentication") 58 | flags.BoolVar(&p.History, argHistory, false, "Scan pages history") 59 | err := confluenceCmd.MarkFlagRequired(argUrl) 60 | if err != nil { 61 | return nil, fmt.Errorf("error while marking '%s' flag as required: %w", argUrl, err) 62 | } 63 | 64 | confluenceCmd.Run = func(cmd *cobra.Command, args []string) { 65 | err := p.initialize(cmd) 66 | if err != nil { 67 | channels.Errors <- fmt.Errorf("error while initializing confluence plugin: %w", err) 68 | return 69 | } 70 | 71 | p.getItems(channels.Items, channels.Errors, channels.WaitGroup) 72 | } 73 | 74 | return confluenceCmd, nil 75 | } 76 | 77 | func (p *ConfluencePlugin) initialize(cmd *cobra.Command) error { 78 | 79 | p.URL = strings.TrimRight(p.URL, "/") 80 | 81 | if p.Username == "" || p.Token == "" { 82 | log.Warn().Msg("confluence credentials were not provided. The scan will be made anonymously only for the public pages") 83 | } 84 | 85 | p.Limit = make(chan struct{}, confluenceMaxRequests) 86 | return nil 87 | } 88 | 89 | func (p *ConfluencePlugin) getItems(items chan Item, errs chan error, wg *sync.WaitGroup) { 90 | p.getSpacesItems(items, errs, wg) 91 | } 92 | 93 | func (p *ConfluencePlugin) getSpacesItems(items chan Item, errs chan error, wg *sync.WaitGroup) { 94 | spaces, err := p.getSpaces() 95 | if err != nil { 96 | errs <- err 97 | } 98 | 99 | for _, space := range spaces { 100 | go p.getSpaceItems(items, errs, wg, space) 101 | wg.Add(1) 102 | } 103 | } 104 | 105 | func (p *ConfluencePlugin) getSpaceItems(items chan Item, errs chan error, wg *sync.WaitGroup, space ConfluenceSpaceResult) { 106 | defer wg.Done() 107 | 108 | pages, err := p.getPages(space) 109 | if err != nil { 110 | errs <- err 111 | return 112 | } 113 | 114 | for _, page := range pages.Pages { 115 | wg.Add(1) 116 | p.Limit <- struct{}{} 117 | go func(page ConfluencePage) { 118 | p.getPageItems(items, errs, wg, page, space) 119 | <-p.Limit 120 | }(page) 121 | } 122 | } 123 | 124 | func (p *ConfluencePlugin) getSpaces() ([]ConfluenceSpaceResult, error) { 125 | totalSpaces, err := p.getSpacesRequest(0) 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | actualSize := totalSpaces.Size 131 | 132 | for actualSize == confluenceDefaultWindow { 133 | moreSpaces, err := p.getSpacesRequest(totalSpaces.Size) 134 | if err != nil { 135 | return nil, err 136 | } 137 | 138 | totalSpaces.Results = append(totalSpaces.Results, moreSpaces.Results...) 139 | totalSpaces.Size += moreSpaces.Size 140 | actualSize = moreSpaces.Size 141 | } 142 | 143 | if len(p.Spaces) == 0 { 144 | log.Info().Msgf(" Total of all %d Spaces detected", len(totalSpaces.Results)) 145 | return totalSpaces.Results, nil 146 | } 147 | 148 | filteredSpaces := make([]ConfluenceSpaceResult, 0) 149 | if len(p.Spaces) > 0 { 150 | for _, space := range totalSpaces.Results { 151 | for _, spaceToScan := range p.Spaces { 152 | if space.Key == spaceToScan || space.Name == spaceToScan || fmt.Sprintf("%d", space.ID) == spaceToScan { 153 | filteredSpaces = append(filteredSpaces, space) 154 | } 155 | } 156 | } 157 | } 158 | 159 | log.Info().Msgf(" Total of filtered %d Spaces detected", len(filteredSpaces)) 160 | return filteredSpaces, nil 161 | } 162 | 163 | func (p *ConfluencePlugin) getSpacesRequest(start int) (*ConfluenceSpaceResponse, error) { 164 | url := fmt.Sprintf("%s/rest/api/space?start=%d", p.URL, start) 165 | body, _, err := lib.HttpRequest(http.MethodGet, url, p) 166 | if err != nil { 167 | return nil, fmt.Errorf("unexpected error creating an http request %w", err) 168 | } 169 | 170 | response := &ConfluenceSpaceResponse{} 171 | jsonErr := json.Unmarshal(body, response) 172 | if jsonErr != nil { 173 | return nil, fmt.Errorf("could not unmarshal response %w", err) 174 | } 175 | 176 | return response, nil 177 | } 178 | 179 | func (p *ConfluencePlugin) getPages(space ConfluenceSpaceResult) (*ConfluencePageResult, error) { 180 | totalPages, err := p.getPagesRequest(space, 0) 181 | 182 | if err != nil { 183 | return nil, fmt.Errorf("unexpected error creating an http request %w", err) 184 | } 185 | 186 | actualSize := len(totalPages.Pages) 187 | 188 | for actualSize == confluenceDefaultWindow { 189 | morePages, err := p.getPagesRequest(space, len(totalPages.Pages)) 190 | 191 | if err != nil { 192 | return nil, fmt.Errorf("unexpected error creating an http request %w", err) 193 | } 194 | 195 | totalPages.Pages = append(totalPages.Pages, morePages.Pages...) 196 | actualSize = len(morePages.Pages) 197 | } 198 | 199 | log.Info().Msgf(" Space - %s have %d pages", space.Name, len(totalPages.Pages)) 200 | 201 | return totalPages, nil 202 | } 203 | 204 | func (p *ConfluencePlugin) getPagesRequest(space ConfluenceSpaceResult, start int) (*ConfluencePageResult, error) { 205 | url := fmt.Sprintf("%s/rest/api/space/%s/content?start=%d", p.URL, space.Key, start) 206 | body, _, err := lib.HttpRequest(http.MethodGet, url, p) 207 | 208 | if err != nil { 209 | return nil, fmt.Errorf("unexpected error creating an http request %w", err) 210 | } 211 | 212 | response := ConfluencePageResponse{} 213 | jsonErr := json.Unmarshal(body, &response) 214 | if jsonErr != nil { 215 | return nil, fmt.Errorf("could not unmarshal response %w", err) 216 | } 217 | 218 | return &response.Results, nil 219 | } 220 | 221 | func (p *ConfluencePlugin) getPageItems(items chan Item, errs chan error, wg *sync.WaitGroup, page ConfluencePage, space ConfluenceSpaceResult) { 222 | defer wg.Done() 223 | 224 | actualPage, previousVersion, err := p.getItem(page, space, 0) 225 | if err != nil { 226 | errs <- err 227 | return 228 | } 229 | items <- *actualPage 230 | 231 | // If older versions exist & run history is true 232 | for previousVersion > 0 && p.History { 233 | actualPage, previousVersion, err = p.getItem(page, space, previousVersion) 234 | if err != nil { 235 | errs <- err 236 | return 237 | } 238 | items <- *actualPage 239 | } 240 | } 241 | 242 | func (p *ConfluencePlugin) getItem(page ConfluencePage, space ConfluenceSpaceResult, version int) (*Item, int, error) { 243 | var url string 244 | var originalUrl string 245 | 246 | // If no version given get the latest, else get the specified version 247 | if version == 0 { 248 | url = fmt.Sprintf("%s/rest/api/content/%s?expand=body.storage.value,version,history.previousVersion", p.URL, page.ID) 249 | originalUrl = fmt.Sprintf("%s/spaces/%s/pages/%s", p.URL, space.Key, page.ID) 250 | 251 | } else { 252 | url = fmt.Sprintf("%s/rest/api/content/%s?status=historical&version=%d&expand=body.storage.value,version,history.previousVersion", p.URL, page.ID, version) 253 | originalUrl = fmt.Sprintf("%s/pages/viewpage.action?pageid=%spageVersion=%d", p.URL, page.ID, version) 254 | } 255 | 256 | request, _, err := lib.HttpRequest(http.MethodGet, url, p) 257 | if err != nil { 258 | return nil, 0, fmt.Errorf("unexpected error creating an http request %w", err) 259 | } 260 | pageContent := ConfluencePageContent{} 261 | jsonErr := json.Unmarshal(request, &pageContent) 262 | if jsonErr != nil { 263 | return nil, 0, jsonErr 264 | } 265 | 266 | content := &Item{ 267 | Content: pageContent.Body.Storage.Value, 268 | ID: originalUrl, 269 | } 270 | return content, pageContent.History.PreviousVersion.Number, nil 271 | } 272 | 273 | type ConfluenceSpaceResult struct { 274 | ID int `json:"id"` 275 | Key string `json:"key"` 276 | Name string `json:"Name"` 277 | Links map[string]string `json:"_links"` 278 | } 279 | 280 | type ConfluenceSpaceResponse struct { 281 | Results []ConfluenceSpaceResult `json:"results"` 282 | Size int `json:"size"` 283 | } 284 | 285 | type ConfluencePageContent struct { 286 | Body struct { 287 | Storage struct { 288 | Value string `json:"value"` 289 | } `json:"storage"` 290 | } `json:"body"` 291 | History struct { 292 | PreviousVersion struct { 293 | Number int 294 | } `json:"previousVersion"` 295 | } `json:"history"` 296 | Version struct { 297 | Number int `json:"number"` 298 | } `json:"version"` 299 | } 300 | 301 | type ConfluencePage struct { 302 | ID string `json:"id"` 303 | Type string `json:"type"` 304 | Title string `json:"title"` 305 | } 306 | 307 | type ConfluencePageResult struct { 308 | Pages []ConfluencePage `json:"results"` 309 | } 310 | 311 | type ConfluencePageResponse struct { 312 | Results ConfluencePageResult `json:"page"` 313 | } 314 | -------------------------------------------------------------------------------- /plugins/paligo.go: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "github.com/checkmarx/2ms/lib" 13 | "github.com/rs/zerolog/log" 14 | "github.com/spf13/cobra" 15 | "golang.org/x/time/rate" 16 | ) 17 | 18 | const ( 19 | paligoInstanceFlag = "instance" 20 | paligoUsernameFlag = "username" 21 | paligoTokenFlag = "token" 22 | paligoAuthFlag = "auth" 23 | paligoFolderFlag = "folder" 24 | ) 25 | 26 | var ( 27 | paligoInstanceArg string 28 | paligoFolderArg int 29 | ) 30 | 31 | type PaligoPlugin struct { 32 | Plugin 33 | Channels 34 | 35 | username string 36 | token string 37 | auth string 38 | 39 | paligoApi *PaligoClient 40 | } 41 | 42 | func (p *PaligoPlugin) GetCredentials() (string, string) { 43 | return p.username, p.token 44 | } 45 | 46 | func (p *PaligoPlugin) GetAuthorizationHeader() string { 47 | if p.auth != "" { 48 | return fmt.Sprintf("Basic %s", p.auth) 49 | } 50 | return lib.CreateBasicAuthCredentials(p) 51 | } 52 | 53 | func (p *PaligoPlugin) GetName() string { 54 | return "paligo" 55 | } 56 | 57 | func (p *PaligoPlugin) DefineCommand(channels Channels) (*cobra.Command, error) { 58 | p.Channels = channels 59 | 60 | command := &cobra.Command{ 61 | Use: fmt.Sprintf("%s --%s %s --%s %s --%s %s", 62 | p.GetName(), 63 | paligoInstanceFlag, strings.ToUpper(paligoInstanceFlag), 64 | paligoUsernameFlag, strings.ToUpper(paligoUsernameFlag), 65 | paligoTokenFlag, strings.ToUpper(paligoTokenFlag)), 66 | Short: "Scan Paligo instance", 67 | Long: "Scan Paligo instance for sensitive information.", 68 | Run: func(cmd *cobra.Command, args []string) { 69 | // Waits for MarkFlagsOneRequired https://github.com/spf13/cobra/pull/1952 70 | if p.auth == "" && (p.username == "" || p.token == "") { 71 | p.Channels.Errors <- fmt.Errorf("exactly one of the flags in the group %v must be set; none were set", []string{paligoAuthFlag, paligoUsernameFlag, paligoTokenFlag}) 72 | return 73 | } 74 | log.Info().Msg("Paligo plugin started") 75 | p.getItems() 76 | }, 77 | } 78 | 79 | command.Flags().StringVar(&paligoInstanceArg, paligoInstanceFlag, "", "Paligo instance name [required]") 80 | err := command.MarkFlagRequired(paligoInstanceFlag) 81 | if err != nil { 82 | return nil, fmt.Errorf("error while marking flag %s as required: %w", paligoInstanceFlag, err) 83 | } 84 | 85 | command.Flags().StringVar(&p.username, paligoUsernameFlag, "", "Paligo username") 86 | command.Flags().StringVar(&p.token, paligoTokenFlag, "", "Paligo token") 87 | command.MarkFlagsRequiredTogether(paligoUsernameFlag, paligoTokenFlag) 88 | 89 | command.Flags().StringVar(&p.auth, paligoAuthFlag, "", "Paligo encoded username:password") 90 | command.MarkFlagsMutuallyExclusive(paligoUsernameFlag, paligoAuthFlag) 91 | command.MarkFlagsMutuallyExclusive(paligoTokenFlag, paligoAuthFlag) 92 | 93 | command.Flags().IntVar(&paligoFolderArg, paligoFolderFlag, 0, "Paligo folder ID") 94 | 95 | return command, nil 96 | } 97 | 98 | func (p *PaligoPlugin) getItems() { 99 | p.paligoApi = newPaligoApi(paligoInstanceArg, p) 100 | 101 | foldersToProcess, err := p.getFirstProcessingFolders() 102 | if err != nil { 103 | p.Channels.Errors <- err 104 | return 105 | } 106 | 107 | itemsChan := p.processFolders(foldersToProcess) 108 | 109 | p.WaitGroup.Add(1) 110 | go func() { 111 | defer p.WaitGroup.Done() 112 | for item := range itemsChan { 113 | p.handleComponent(item) 114 | } 115 | }() 116 | } 117 | 118 | func (p *PaligoPlugin) getFirstProcessingFolders() ([]PaligoItem, error) { 119 | foldersToProcess := []PaligoItem{} 120 | 121 | if paligoFolderArg != 0 { 122 | foldersToProcess = append(foldersToProcess, PaligoItem{ID: paligoFolderArg, Name: "ID" + fmt.Sprint(paligoFolderArg)}) 123 | } else { 124 | folders, err := p.paligoApi.listFolders() 125 | if err != nil { 126 | log.Error().Err(err).Msg("error while getting root folders") 127 | return nil, fmt.Errorf("error while getting root folders: %w", err) 128 | } 129 | for _, folder := range *folders { 130 | foldersToProcess = append(foldersToProcess, folder.PaligoItem) 131 | } 132 | } 133 | return foldersToProcess, nil 134 | } 135 | 136 | func (p *PaligoPlugin) processFolders(foldersToProcess []PaligoItem) chan PaligoItem { 137 | 138 | itemsChan := make(chan PaligoItem) 139 | 140 | p.WaitGroup.Add(1) 141 | go func() { 142 | defer p.WaitGroup.Done() 143 | 144 | for len(foldersToProcess) > 0 { 145 | folder := foldersToProcess[0] 146 | foldersToProcess = foldersToProcess[1:] 147 | 148 | log.Info().Msgf("Getting folder %s", folder.Name) 149 | folderInfo, err := p.paligoApi.showFolder(folder.ID) 150 | if err != nil { 151 | log.Error().Err(err).Msgf("error while getting %s '%s'", folder.Type, folder.Name) 152 | p.Channels.Errors <- err 153 | continue 154 | } 155 | 156 | for _, child := range folderInfo.Children { 157 | if child.Type == "component" { 158 | itemsChan <- child 159 | } else if child.Type == "folder" { 160 | foldersToProcess = append(foldersToProcess, child) 161 | } 162 | } 163 | } 164 | close(itemsChan) 165 | }() 166 | 167 | return itemsChan 168 | } 169 | 170 | func (p *PaligoPlugin) handleComponent(item PaligoItem) { 171 | 172 | log.Info().Msgf("Getting component %s", item.Name) 173 | document, err := p.paligoApi.showDocument(item.ID) 174 | if err != nil { 175 | log.Error().Err(err).Msgf("error while getting document '%s'", item.Name) 176 | p.Channels.Errors <- fmt.Errorf("error while getting document '%s': %w", item.Name, err) 177 | return 178 | } 179 | 180 | url := fmt.Sprintf("https://%s.paligoapp.com/document/edit/%d", p.paligoApi.Instance, document.ID) 181 | 182 | p.Items <- Item{ 183 | Content: document.Content, 184 | ID: url, 185 | } 186 | } 187 | 188 | /** 189 | * Paligo API 190 | */ 191 | 192 | // https://paligo.net/docs/apidocs/en/index-en.html#UUID-a5b548af-9a37-d305-f5a8-11142d86fe20 193 | const ( 194 | PALIGO_RATE_LIMIT_CHECK_INTERVAL = 5 * time.Second 195 | PALIGO_DOCUMENT_SHOW_LIMIT = 50 196 | PALIGO_FOLDER_SHOW_LIMIT = 50 197 | ) 198 | 199 | func rateLimitPerSecond(rateLimit int) rate.Limit { 200 | return rate.Every(time.Minute / time.Duration(rateLimit)) 201 | } 202 | 203 | type PaligoItem struct { 204 | ID int `json:"id"` 205 | Name string `json:"name"` 206 | UUID string `json:"uuid"` 207 | Type string `json:"type"` 208 | } 209 | 210 | type Folder struct { 211 | PaligoItem 212 | Children []PaligoItem `json:"children"` 213 | } 214 | 215 | type EmptyFolder struct { 216 | PaligoItem 217 | Children string `json:"children"` 218 | } 219 | 220 | type Component struct { 221 | PaligoItem 222 | Subtype string `json:"subtype"` 223 | Creator int `json:"creator"` 224 | Owner int `json:"owner"` 225 | Author int `json:"author"` 226 | CreatedAt int `json:"created_at"` 227 | ModifiedAt int `json:"modified_at"` 228 | Checkout bool `json:"checkout"` 229 | CheckoutUser string `json:"checkout_user"` 230 | ParentResource int `json:"parent_resource"` 231 | Taxonomies []interface{} `json:"taxonomies"` 232 | ReleaseStatus string `json:"release_status"` 233 | Content string `json:"content"` 234 | Languages []string `json:"languages"` 235 | External []interface{} `json:"external"` 236 | CustomAttributes []interface{} `json:"custom_attributes"` 237 | } 238 | 239 | type ListFoldersResponse struct { 240 | Page int `json:"page"` 241 | NextPage string `json:"next_page"` 242 | TotalPages int `json:"total_pages"` 243 | Folders []EmptyFolder `json:"folders"` 244 | } 245 | 246 | type Document struct { 247 | PaligoItem 248 | Content string `json:"content"` 249 | Languages []string `json:"languages"` 250 | } 251 | 252 | type PaligoClient struct { 253 | Instance string 254 | auth lib.IAuthorizationHeader 255 | 256 | foldersLimiter *rate.Limiter 257 | documentsLimiter *rate.Limiter 258 | } 259 | 260 | func reserveRateLimit(response *http.Response, lim *rate.Limiter, err error) error { 261 | if response.StatusCode != 429 { 262 | return err 263 | } 264 | 265 | rateLimit := response.Header.Get("Retry-After") 266 | if rateLimit == "" { 267 | return fmt.Errorf("Retry-After header not found") 268 | } 269 | seconds, err := strconv.Atoi(rateLimit) 270 | if err != nil { 271 | return fmt.Errorf("error parsing Retry-After header: %w", err) 272 | } 273 | log.Warn().Msgf("Rate limit exceeded, need to wait for %d seconds", seconds) 274 | lim.SetBurst(1) 275 | time.Sleep(time.Second * time.Duration(seconds)) 276 | return nil 277 | } 278 | 279 | func (p *PaligoClient) request(endpoint string, lim *rate.Limiter) ([]byte, error) { 280 | if err := lim.Wait(context.Background()); err != nil { 281 | log.Error().Msgf("Error waiting for rate limiter: %s", err) 282 | return nil, err 283 | } 284 | 285 | url := fmt.Sprintf("https://%s.paligoapp.com/api/v2/%s", p.Instance, endpoint) 286 | body, response, err := lib.HttpRequest("GET", url, p.auth) 287 | if err != nil { 288 | if err := reserveRateLimit(response, lim, err); err != nil { 289 | return nil, err 290 | } 291 | return p.request(endpoint, lim) 292 | } 293 | 294 | return body, nil 295 | } 296 | 297 | func (p *PaligoClient) listFolders() (*[]EmptyFolder, error) { 298 | body, err := p.request("folders", p.foldersLimiter) 299 | if err != nil { 300 | return nil, err 301 | } 302 | 303 | var folders *ListFoldersResponse 304 | err = json.Unmarshal(body, &folders) 305 | 306 | return &folders.Folders, err 307 | } 308 | 309 | func (p *PaligoClient) showFolder(folderId int) (*Folder, error) { 310 | body, err := p.request(fmt.Sprintf("folders/%d", folderId), p.foldersLimiter) 311 | if err != nil { 312 | return nil, err 313 | } 314 | 315 | folder := &Folder{} 316 | err = json.Unmarshal(body, folder) 317 | 318 | return folder, err 319 | } 320 | 321 | func (p *PaligoClient) showDocument(documentId int) (*Document, error) { 322 | body, err := p.request(fmt.Sprintf("documents/%d", documentId), p.documentsLimiter) 323 | if err != nil { 324 | return nil, err 325 | } 326 | 327 | document := &Document{} 328 | err = json.Unmarshal(body, document) 329 | 330 | return document, err 331 | } 332 | 333 | func newPaligoApi(instance string, auth lib.IAuthorizationHeader) *PaligoClient { 334 | return &PaligoClient{ 335 | Instance: instance, 336 | auth: auth, 337 | 338 | foldersLimiter: rate.NewLimiter(rateLimitPerSecond(PALIGO_FOLDER_SHOW_LIMIT), PALIGO_FOLDER_SHOW_LIMIT), 339 | documentsLimiter: rate.NewLimiter(rateLimitPerSecond(PALIGO_DOCUMENT_SHOW_LIMIT), PALIGO_DOCUMENT_SHOW_LIMIT), 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /secrets/secrets.go: -------------------------------------------------------------------------------- 1 | package secrets 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "regexp" 7 | "strings" 8 | "sync" 9 | 10 | "github.com/checkmarx/2ms/plugins" 11 | "github.com/checkmarx/2ms/reporting" 12 | "github.com/zricethezav/gitleaks/v8/cmd/generate/config/rules" 13 | "github.com/zricethezav/gitleaks/v8/config" 14 | "github.com/zricethezav/gitleaks/v8/detect" 15 | ) 16 | 17 | type Secrets struct { 18 | rules map[string]config.Rule 19 | detector detect.Detector 20 | } 21 | 22 | type Rule struct { 23 | Rule config.Rule 24 | Tags []string 25 | } 26 | 27 | const TagApiKey = "api-key" 28 | const TagClientId = "client-id" 29 | const TagClientSecret = "client-secret" 30 | const TagSecretKey = "secret-key" 31 | const TagAccessKey = "access-key" 32 | const TagAccessId = "access-id" 33 | const TagApiToken = "api-token" 34 | const TagAccessToken = "access-token" 35 | const TagRefreshToken = "refresh-token" 36 | const TagPrivateKey = "private-key" 37 | const TagPublicKey = "public-key" 38 | const TagEncryptionKey = "encryption-key" 39 | const TagTriggerToken = "trigger-token" 40 | const TagRegistrationToken = "registration-token" 41 | const TagPassword = "password" 42 | const TagUploadToken = "upload-token" 43 | const TagPublicSecret = "public-secret" 44 | const TagSensitiveUrl = "sensitive-url" 45 | const TagWebhook = "webhook" 46 | 47 | const customRegexRuleIdFormat = "custom-regex-%d" 48 | 49 | func Init(tags []string) *Secrets { 50 | 51 | allRules, _ := loadAllRules() 52 | rulesToBeApplied := getRules(allRules, tags) 53 | 54 | config := config.Config{ 55 | Rules: rulesToBeApplied, 56 | } 57 | 58 | detector := detect.NewDetector(config) 59 | 60 | return &Secrets{ 61 | rules: rulesToBeApplied, 62 | detector: *detector, 63 | } 64 | } 65 | 66 | func (s *Secrets) Detect(secretsChannel chan reporting.Secret, item plugins.Item, wg *sync.WaitGroup) { 67 | defer wg.Done() 68 | 69 | fragment := detect.Fragment{ 70 | Raw: item.Content, 71 | } 72 | for _, value := range s.detector.Detect(fragment) { 73 | itemId := getItemId(item.ID) 74 | secretsChannel <- reporting.Secret{ID: itemId, Source: item.ID, Description: value.Description, StartLine: value.StartLine, StartColumn: value.StartColumn, EndLine: value.EndLine, EndColumn: value.EndColumn, Value: value.Secret} 75 | } 76 | } 77 | 78 | func (s *Secrets) AddRegexRules(patterns []string) error { 79 | for idx, pattern := range patterns { 80 | regex, err := regexp.Compile(pattern) 81 | if err != nil { 82 | return fmt.Errorf("failed to compile regex rule %s: %w", pattern, err) 83 | } 84 | rule := config.Rule{ 85 | Description: "Custom Regex Rule From User", 86 | RuleID: fmt.Sprintf(customRegexRuleIdFormat, idx+1), 87 | Regex: regex, 88 | Keywords: []string{}, 89 | } 90 | s.rules[rule.RuleID] = rule 91 | } 92 | return nil 93 | } 94 | 95 | func getItemId(fullPath string) string { 96 | var itemId string 97 | if strings.Contains(fullPath, "/") { 98 | itemLinkStrings := strings.Split(fullPath, "/") 99 | itemId = itemLinkStrings[len(itemLinkStrings)-1] 100 | } 101 | if strings.Contains(fullPath, "\\") { 102 | itemId = filepath.Base(fullPath) 103 | } 104 | return itemId 105 | } 106 | 107 | func getRules(allRules []Rule, tags []string) map[string]config.Rule { 108 | rulesToBeApplied := make(map[string]config.Rule) 109 | 110 | if isAllFilter(tags) { 111 | // ensure rules have unique ids 112 | for _, rule := range allRules { 113 | // required to be empty when not running via cli. otherwise rule will be ignored 114 | rule.Rule.Keywords = []string{} 115 | rulesToBeApplied[rule.Rule.RuleID] = rule.Rule 116 | } 117 | } else { 118 | for _, rule := range allRules { 119 | rule.Rule.Keywords = []string{} 120 | for _, userTag := range tags { 121 | for _, ruleTag := range rule.Tags { 122 | if strings.EqualFold(ruleTag, userTag) { 123 | rulesToBeApplied[rule.Rule.RuleID] = rule.Rule 124 | } 125 | } 126 | } 127 | } 128 | } 129 | return rulesToBeApplied 130 | } 131 | 132 | func isAllFilter(rulesFilter []string) bool { 133 | for _, filter := range rulesFilter { 134 | if strings.EqualFold(filter, "all") { 135 | return true 136 | } 137 | } 138 | return false 139 | } 140 | 141 | func loadAllRules() ([]Rule, error) { 142 | var allRules []Rule 143 | allRules = make([]Rule, 0) 144 | 145 | allRules = append(allRules, Rule{Rule: *rules.AdafruitAPIKey(), Tags: []string{TagApiKey}}) 146 | allRules = append(allRules, Rule{Rule: *rules.AdobeClientID(), Tags: []string{TagClientId}}) 147 | allRules = append(allRules, Rule{Rule: *rules.AdobeClientSecret(), Tags: []string{TagClientSecret}}) 148 | allRules = append(allRules, Rule{Rule: *rules.AgeSecretKey(), Tags: []string{TagSecretKey}}) 149 | allRules = append(allRules, Rule{Rule: *rules.Airtable(), Tags: []string{TagApiKey}}) 150 | allRules = append(allRules, Rule{Rule: *rules.AlgoliaApiKey(), Tags: []string{TagApiKey}}) 151 | allRules = append(allRules, Rule{Rule: *rules.AlibabaAccessKey(), Tags: []string{TagAccessKey, TagAccessId}}) 152 | allRules = append(allRules, Rule{Rule: *rules.AlibabaSecretKey(), Tags: []string{TagSecretKey}}) 153 | allRules = append(allRules, Rule{Rule: *rules.AsanaClientID(), Tags: []string{TagClientId}}) 154 | allRules = append(allRules, Rule{Rule: *rules.AsanaClientSecret(), Tags: []string{TagClientSecret}}) 155 | allRules = append(allRules, Rule{Rule: *rules.Atlassian(), Tags: []string{TagApiToken}}) 156 | allRules = append(allRules, Rule{Rule: *rules.AWS(), Tags: []string{TagAccessToken}}) 157 | allRules = append(allRules, Rule{Rule: *rules.BitBucketClientID(), Tags: []string{TagClientId}}) 158 | allRules = append(allRules, Rule{Rule: *rules.BitBucketClientSecret(), Tags: []string{TagClientSecret}}) 159 | allRules = append(allRules, Rule{Rule: *rules.BittrexAccessKey(), Tags: []string{TagAccessKey}}) 160 | allRules = append(allRules, Rule{Rule: *rules.BittrexSecretKey(), Tags: []string{TagSecretKey}}) 161 | allRules = append(allRules, Rule{Rule: *rules.Beamer(), Tags: []string{TagApiToken}}) 162 | allRules = append(allRules, Rule{Rule: *rules.CodecovAccessToken(), Tags: []string{TagAccessToken}}) 163 | allRules = append(allRules, Rule{Rule: *rules.CoinbaseAccessToken(), Tags: []string{TagAccessToken}}) 164 | allRules = append(allRules, Rule{Rule: *rules.Clojars(), Tags: []string{TagApiToken}}) 165 | allRules = append(allRules, Rule{Rule: *rules.ConfluentAccessToken(), Tags: []string{TagAccessToken}}) 166 | allRules = append(allRules, Rule{Rule: *rules.ConfluentSecretKey(), Tags: []string{TagSecretKey}}) 167 | allRules = append(allRules, Rule{Rule: *rules.Contentful(), Tags: []string{TagApiToken}}) 168 | allRules = append(allRules, Rule{Rule: *rules.Databricks(), Tags: []string{TagApiToken}}) 169 | allRules = append(allRules, Rule{Rule: *rules.DatadogtokenAccessToken(), Tags: []string{TagAccessToken}}) 170 | allRules = append(allRules, Rule{Rule: *rules.DigitalOceanPAT(), Tags: []string{TagAccessToken}}) 171 | allRules = append(allRules, Rule{Rule: *rules.DigitalOceanOAuthToken(), Tags: []string{TagAccessToken}}) 172 | allRules = append(allRules, Rule{Rule: *rules.DigitalOceanRefreshToken(), Tags: []string{TagRefreshToken}}) 173 | allRules = append(allRules, Rule{Rule: *rules.DiscordAPIToken(), Tags: []string{TagApiKey, TagApiToken}}) 174 | allRules = append(allRules, Rule{Rule: *rules.DiscordClientID(), Tags: []string{TagClientId}}) 175 | allRules = append(allRules, Rule{Rule: *rules.DiscordClientSecret(), Tags: []string{TagClientSecret}}) 176 | allRules = append(allRules, Rule{Rule: *rules.Doppler(), Tags: []string{TagApiToken}}) 177 | allRules = append(allRules, Rule{Rule: *rules.DropBoxAPISecret(), Tags: []string{TagApiToken}}) 178 | allRules = append(allRules, Rule{Rule: *rules.DropBoxShortLivedAPIToken(), Tags: []string{TagApiToken}}) 179 | allRules = append(allRules, Rule{Rule: *rules.DropBoxLongLivedAPIToken(), Tags: []string{TagApiToken}}) 180 | allRules = append(allRules, Rule{Rule: *rules.DroneciAccessToken(), Tags: []string{TagAccessToken}}) 181 | allRules = append(allRules, Rule{Rule: *rules.DatadogtokenAccessToken(), Tags: []string{TagClientId}}) 182 | allRules = append(allRules, Rule{Rule: *rules.Duffel(), Tags: []string{TagApiToken}}) 183 | allRules = append(allRules, Rule{Rule: *rules.Dynatrace(), Tags: []string{TagApiToken}}) 184 | allRules = append(allRules, Rule{Rule: *rules.EasyPost(), Tags: []string{TagApiToken}}) 185 | allRules = append(allRules, Rule{Rule: *rules.EasyPostTestAPI(), Tags: []string{TagApiToken}}) 186 | allRules = append(allRules, Rule{Rule: *rules.EtsyAccessToken(), Tags: []string{TagAccessToken}}) 187 | allRules = append(allRules, Rule{Rule: *rules.Facebook(), Tags: []string{TagApiToken}}) 188 | allRules = append(allRules, Rule{Rule: *rules.FastlyAPIToken(), Tags: []string{TagApiToken, TagApiKey}}) 189 | allRules = append(allRules, Rule{Rule: *rules.FinicityClientSecret(), Tags: []string{TagClientSecret}}) 190 | allRules = append(allRules, Rule{Rule: *rules.FinicityAPIToken(), Tags: []string{TagApiToken}}) 191 | allRules = append(allRules, Rule{Rule: *rules.FlickrAccessToken(), Tags: []string{TagAccessToken}}) 192 | allRules = append(allRules, Rule{Rule: *rules.FinnhubAccessToken(), Tags: []string{TagAccessToken}}) 193 | allRules = append(allRules, Rule{Rule: *rules.FlutterwavePublicKey(), Tags: []string{TagPublicKey}}) 194 | allRules = append(allRules, Rule{Rule: *rules.FlutterwaveSecretKey(), Tags: []string{TagSecretKey}}) 195 | allRules = append(allRules, Rule{Rule: *rules.FlutterwaveEncKey(), Tags: []string{TagEncryptionKey}}) 196 | allRules = append(allRules, Rule{Rule: *rules.FrameIO(), Tags: []string{TagApiToken}}) 197 | allRules = append(allRules, Rule{Rule: *rules.FreshbooksAccessToken(), Tags: []string{TagAccessToken}}) 198 | allRules = append(allRules, Rule{Rule: *rules.GCPAPIKey(), Tags: []string{TagApiKey}}) 199 | allRules = append(allRules, Rule{Rule: *rules.GenericCredential(), Tags: []string{TagApiKey}}) 200 | allRules = append(allRules, Rule{Rule: *rules.GitHubPat(), Tags: []string{TagAccessToken}}) 201 | allRules = append(allRules, Rule{Rule: *rules.GitHubFineGrainedPat(), Tags: []string{TagAccessToken}}) 202 | allRules = append(allRules, Rule{Rule: *rules.GitHubOauth(), Tags: []string{TagAccessToken}}) 203 | allRules = append(allRules, Rule{Rule: *rules.GitHubApp(), Tags: []string{TagAccessToken}}) 204 | allRules = append(allRules, Rule{Rule: *rules.GitHubRefresh(), Tags: []string{TagRefreshToken}}) 205 | allRules = append(allRules, Rule{Rule: *rules.GitlabPat(), Tags: []string{TagAccessToken}}) 206 | allRules = append(allRules, Rule{Rule: *rules.GitlabPipelineTriggerToken(), Tags: []string{TagTriggerToken}}) 207 | allRules = append(allRules, Rule{Rule: *rules.GitlabRunnerRegistrationToken(), Tags: []string{TagRegistrationToken}}) 208 | allRules = append(allRules, Rule{Rule: *rules.GitterAccessToken(), Tags: []string{TagAccessToken}}) 209 | allRules = append(allRules, Rule{Rule: *rules.GoCardless(), Tags: []string{TagApiToken}}) 210 | allRules = append(allRules, Rule{Rule: *rules.GrafanaApiKey(), Tags: []string{TagApiKey}}) 211 | allRules = append(allRules, Rule{Rule: *rules.GrafanaCloudApiToken(), Tags: []string{TagApiToken}}) 212 | allRules = append(allRules, Rule{Rule: *rules.GrafanaServiceAccountToken(), Tags: []string{TagAccessToken}}) 213 | allRules = append(allRules, Rule{Rule: *rules.Hashicorp(), Tags: []string{TagApiToken}}) 214 | allRules = append(allRules, Rule{Rule: *rules.Heroku(), Tags: []string{TagApiKey}}) 215 | allRules = append(allRules, Rule{Rule: *rules.HubSpot(), Tags: []string{TagApiToken, TagApiKey}}) 216 | allRules = append(allRules, Rule{Rule: *rules.Intercom(), Tags: []string{TagApiToken, TagApiKey}}) 217 | allRules = append(allRules, Rule{Rule: *rules.JWT(), Tags: []string{TagAccessToken}}) 218 | allRules = append(allRules, Rule{Rule: *rules.KrakenAccessToken(), Tags: []string{TagAccessToken}}) 219 | allRules = append(allRules, Rule{Rule: *rules.KucoinAccessToken(), Tags: []string{TagAccessToken}}) 220 | allRules = append(allRules, Rule{Rule: *rules.KucoinSecretKey(), Tags: []string{TagSecretKey}}) 221 | allRules = append(allRules, Rule{Rule: *rules.LaunchDarklyAccessToken(), Tags: []string{TagAccessToken}}) 222 | allRules = append(allRules, Rule{Rule: *rules.LinearAPIToken(), Tags: []string{TagApiToken, TagApiKey}}) 223 | allRules = append(allRules, Rule{Rule: *rules.LinearClientSecret(), Tags: []string{TagClientSecret}}) 224 | allRules = append(allRules, Rule{Rule: *rules.LinkedinClientID(), Tags: []string{TagClientId}}) 225 | allRules = append(allRules, Rule{Rule: *rules.LinkedinClientSecret(), Tags: []string{TagClientSecret}}) 226 | allRules = append(allRules, Rule{Rule: *rules.LobAPIToken(), Tags: []string{TagApiKey}}) 227 | allRules = append(allRules, Rule{Rule: *rules.LobPubAPIToken(), Tags: []string{TagApiKey}}) 228 | allRules = append(allRules, Rule{Rule: *rules.MailChimp(), Tags: []string{TagApiKey}}) 229 | allRules = append(allRules, Rule{Rule: *rules.MailGunPubAPIToken(), Tags: []string{TagPublicKey}}) 230 | allRules = append(allRules, Rule{Rule: *rules.MailGunPrivateAPIToken(), Tags: []string{TagPrivateKey}}) 231 | allRules = append(allRules, Rule{Rule: *rules.MailGunSigningKey(), Tags: []string{TagApiKey}}) 232 | allRules = append(allRules, Rule{Rule: *rules.MapBox(), Tags: []string{TagApiToken}}) 233 | allRules = append(allRules, Rule{Rule: *rules.MattermostAccessToken(), Tags: []string{TagAccessToken}}) 234 | allRules = append(allRules, Rule{Rule: *rules.MessageBirdAPIToken(), Tags: []string{TagApiToken}}) 235 | allRules = append(allRules, Rule{Rule: *rules.MessageBirdClientID(), Tags: []string{TagClientId}}) 236 | allRules = append(allRules, Rule{Rule: *rules.NetlifyAccessToken(), Tags: []string{TagAccessToken}}) 237 | allRules = append(allRules, Rule{Rule: *rules.NewRelicUserID(), Tags: []string{TagApiKey}}) 238 | allRules = append(allRules, Rule{Rule: *rules.NewRelicUserKey(), Tags: []string{TagAccessId}}) 239 | allRules = append(allRules, Rule{Rule: *rules.NewRelicBrowserAPIKey(), Tags: []string{TagApiToken}}) 240 | allRules = append(allRules, Rule{Rule: *rules.NPM(), Tags: []string{TagAccessToken}}) 241 | allRules = append(allRules, Rule{Rule: *rules.NytimesAccessToken(), Tags: []string{TagAccessToken}}) 242 | allRules = append(allRules, Rule{Rule: *rules.OktaAccessToken(), Tags: []string{TagAccessToken}}) 243 | allRules = append(allRules, Rule{Rule: *rules.PlaidAccessID(), Tags: []string{TagClientId}}) 244 | allRules = append(allRules, Rule{Rule: *rules.PlaidSecretKey(), Tags: []string{TagSecretKey}}) 245 | allRules = append(allRules, Rule{Rule: *rules.PlaidAccessToken(), Tags: []string{TagApiToken}}) 246 | allRules = append(allRules, Rule{Rule: *rules.PlanetScalePassword(), Tags: []string{TagPassword}}) 247 | allRules = append(allRules, Rule{Rule: *rules.PlanetScaleAPIToken(), Tags: []string{TagApiToken}}) 248 | allRules = append(allRules, Rule{Rule: *rules.PlanetScaleOAuthToken(), Tags: []string{TagAccessToken}}) 249 | allRules = append(allRules, Rule{Rule: *rules.PostManAPI(), Tags: []string{TagApiToken}}) 250 | allRules = append(allRules, Rule{Rule: *rules.Prefect(), Tags: []string{TagApiToken}}) 251 | allRules = append(allRules, Rule{Rule: *rules.PrivateKey(), Tags: []string{TagPrivateKey}}) 252 | allRules = append(allRules, Rule{Rule: *rules.PulumiAPIToken(), Tags: []string{TagApiToken}}) 253 | allRules = append(allRules, Rule{Rule: *rules.PyPiUploadToken(), Tags: []string{TagUploadToken}}) 254 | allRules = append(allRules, Rule{Rule: *rules.RapidAPIAccessToken(), Tags: []string{TagAccessToken}}) 255 | allRules = append(allRules, Rule{Rule: *rules.ReadMe(), Tags: []string{TagApiToken}}) 256 | allRules = append(allRules, Rule{Rule: *rules.RubyGemsAPIToken(), Tags: []string{TagApiToken}}) 257 | allRules = append(allRules, Rule{Rule: *rules.SendbirdAccessID(), Tags: []string{TagAccessId}}) 258 | allRules = append(allRules, Rule{Rule: *rules.SendbirdAccessToken(), Tags: []string{TagAccessToken}}) 259 | allRules = append(allRules, Rule{Rule: *rules.SendGridAPIToken(), Tags: []string{TagApiToken}}) 260 | allRules = append(allRules, Rule{Rule: *rules.SendInBlueAPIToken(), Tags: []string{TagApiToken}}) 261 | allRules = append(allRules, Rule{Rule: *rules.SentryAccessToken(), Tags: []string{TagAccessToken}}) 262 | allRules = append(allRules, Rule{Rule: *rules.ShippoAPIToken(), Tags: []string{TagApiToken}}) 263 | allRules = append(allRules, Rule{Rule: *rules.ShopifyAccessToken(), Tags: []string{TagAccessToken}}) 264 | allRules = append(allRules, Rule{Rule: *rules.ShopifyCustomAccessToken(), Tags: []string{TagAccessToken}}) 265 | allRules = append(allRules, Rule{Rule: *rules.ShopifyPrivateAppAccessToken(), Tags: []string{TagAccessToken}}) 266 | allRules = append(allRules, Rule{Rule: *rules.ShopifySharedSecret(), Tags: []string{TagPublicSecret}}) 267 | allRules = append(allRules, Rule{Rule: *rules.SidekiqSecret(), Tags: []string{TagSecretKey}}) 268 | allRules = append(allRules, Rule{Rule: *rules.SidekiqSensitiveUrl(), Tags: []string{TagSensitiveUrl}}) 269 | allRules = append(allRules, Rule{Rule: *rules.SlackAccessToken(), Tags: []string{TagAccessToken}}) 270 | allRules = append(allRules, Rule{Rule: *rules.SlackWebHook(), Tags: []string{TagWebhook}}) 271 | allRules = append(allRules, Rule{Rule: *rules.StripeAccessToken(), Tags: []string{TagAccessToken}}) 272 | allRules = append(allRules, Rule{Rule: *rules.SquareAccessToken(), Tags: []string{TagAccessToken}}) 273 | allRules = append(allRules, Rule{Rule: *rules.SquareSpaceAccessToken(), Tags: []string{TagAccessToken}}) 274 | allRules = append(allRules, Rule{Rule: *rules.SumoLogicAccessID(), Tags: []string{TagAccessId}}) 275 | allRules = append(allRules, Rule{Rule: *rules.SumoLogicAccessToken(), Tags: []string{TagAccessToken}}) 276 | allRules = append(allRules, Rule{Rule: *rules.TeamsWebhook(), Tags: []string{TagWebhook}}) 277 | allRules = append(allRules, Rule{Rule: *rules.TelegramBotToken(), Tags: []string{TagApiToken}}) 278 | allRules = append(allRules, Rule{Rule: *rules.TravisCIAccessToken(), Tags: []string{TagAccessToken}}) 279 | allRules = append(allRules, Rule{Rule: *rules.Twilio(), Tags: []string{TagApiKey}}) 280 | allRules = append(allRules, Rule{Rule: *rules.TwitchAPIToken(), Tags: []string{TagApiToken}}) 281 | allRules = append(allRules, Rule{Rule: *rules.TwitterAPIKey(), Tags: []string{TagApiKey}}) 282 | allRules = append(allRules, Rule{Rule: *rules.TwitterAPISecret(), Tags: []string{TagApiKey}}) 283 | allRules = append(allRules, Rule{Rule: *rules.TwitterAccessToken(), Tags: []string{TagAccessToken}}) 284 | allRules = append(allRules, Rule{Rule: *rules.TwitterAccessSecret(), Tags: []string{TagPublicSecret}}) 285 | allRules = append(allRules, Rule{Rule: *rules.TwitterBearerToken(), Tags: []string{TagApiToken}}) 286 | allRules = append(allRules, Rule{Rule: *rules.Typeform(), Tags: []string{TagApiToken}}) 287 | allRules = append(allRules, Rule{Rule: *rules.VaultBatchToken(), Tags: []string{TagApiToken}}) 288 | allRules = append(allRules, Rule{Rule: *rules.VaultServiceToken(), Tags: []string{TagApiToken}}) 289 | allRules = append(allRules, Rule{Rule: *rules.YandexAPIKey(), Tags: []string{TagApiKey}}) 290 | allRules = append(allRules, Rule{Rule: *rules.YandexAWSAccessToken(), Tags: []string{TagAccessToken}}) 291 | allRules = append(allRules, Rule{Rule: *rules.YandexAccessToken(), Tags: []string{TagAccessToken}}) 292 | allRules = append(allRules, Rule{Rule: *rules.ZendeskSecretKey(), Tags: []string{TagSecretKey}}) 293 | 294 | return allRules, nil 295 | } 296 | -------------------------------------------------------------------------------- /lib/flags_test.go: -------------------------------------------------------------------------------- 1 | package lib_test 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/checkmarx/2ms/lib" 10 | "github.com/spf13/cobra" 11 | "github.com/spf13/viper" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | // TODO: positional arguments 16 | 17 | const envVarPrefix = "PREFIX" 18 | 19 | func TestBindFlags(t *testing.T) { 20 | t.Run("BindFlags_TestEmptyViper", func(t *testing.T) { 21 | assertClearEnv(t) 22 | defer clearEnvVars(t) 23 | 24 | cmd := &cobra.Command{} 25 | v := getViper() 26 | 27 | var ( 28 | testString string 29 | testInt int 30 | testBool bool 31 | testFloat64 float64 32 | ) 33 | 34 | cmd.PersistentFlags().StringVar(&testString, "test-string", "", "Test string flag") 35 | cmd.PersistentFlags().IntVar(&testInt, "test-int", 0, "Test int flag") 36 | cmd.PersistentFlags().BoolVar(&testBool, "test-bool", false, "Test bool flag") 37 | cmd.PersistentFlags().Float64Var(&testFloat64, "test-float64", 0.0, "Test float64 flag") 38 | 39 | err := lib.BindFlags(cmd, v, envVarPrefix) 40 | assert.NoError(t, err) 41 | 42 | assert.Empty(t, testString) 43 | assert.Empty(t, testInt) 44 | assert.Empty(t, testBool) 45 | assert.Empty(t, testFloat64) 46 | }) 47 | 48 | t.Run("BindFlags_FromEnvVarsToCobraCommand", func(t *testing.T) { 49 | assertClearEnv(t) 50 | defer clearEnvVars(t) 51 | 52 | cmd := &cobra.Command{} 53 | v := getViper() 54 | v.SetEnvPrefix(envVarPrefix) 55 | 56 | var ( 57 | testString string 58 | testInt int 59 | testBool bool 60 | testFloat64 float64 61 | ) 62 | 63 | cmd.PersistentFlags().StringVar(&testString, "test-string", "", "Test string flag") 64 | cmd.PersistentFlags().IntVar(&testInt, "test-int", 0, "Test int flag") 65 | cmd.PersistentFlags().BoolVar(&testBool, "test-bool", false, "Test bool flag") 66 | cmd.PersistentFlags().Float64Var(&testFloat64, "test-float64", 0.0, "Test float64 flag") 67 | 68 | err := setEnv("PREFIX_TEST_STRING", "test-string-value") 69 | assert.NoError(t, err) 70 | err = setEnv("PREFIX_TEST_INT", "456") 71 | assert.NoError(t, err) 72 | err = setEnv("PREFIX_TEST_BOOL", "true") 73 | assert.NoError(t, err) 74 | err = setEnv("PREFIX_TEST_FLOAT64", "1.23") 75 | assert.NoError(t, err) 76 | 77 | err = lib.BindFlags(cmd, v, envVarPrefix) 78 | assert.NoError(t, err) 79 | 80 | assert.Equal(t, "test-string-value", testString) 81 | assert.Equal(t, 456, testInt) 82 | assert.Equal(t, true, testBool) 83 | assert.Equal(t, 1.23, testFloat64) 84 | }) 85 | 86 | t.Run("BindFlags_NonPersistentFlags", func(t *testing.T) { 87 | assertClearEnv(t) 88 | defer clearEnvVars(t) 89 | 90 | cmd := &cobra.Command{} 91 | v := getViper() 92 | 93 | var ( 94 | testString string 95 | ) 96 | 97 | cmd.Flags().StringVar(&testString, "test-string", "", "Test string flag") 98 | 99 | err := setEnv("PREFIX_TEST_STRING", "test-string-value") 100 | assert.NoError(t, err) 101 | 102 | err = lib.BindFlags(cmd, v, envVarPrefix) 103 | assert.NoError(t, err) 104 | 105 | assert.Equal(t, "test-string-value", testString) 106 | }) 107 | 108 | t.Run("BindFlags_Subcommand", func(t *testing.T) { 109 | assertClearEnv(t) 110 | defer clearEnvVars(t) 111 | 112 | var ( 113 | testString string 114 | testInt int 115 | ) 116 | 117 | subCommand := &cobra.Command{ 118 | Use: "subCommand", 119 | } 120 | subCommand.Flags().StringVar(&testString, "test-string", "", "Test string flag") 121 | subCommand.PersistentFlags().IntVar(&testInt, "test-int", 0, "Test int flag") 122 | 123 | cmd := &cobra.Command{} 124 | cmd.AddCommand(subCommand) 125 | v := getViper() 126 | 127 | err := setEnv("PREFIX_SUBCOMMAND_TEST_STRING", "test-string-value") 128 | assert.NoError(t, err) 129 | err = setEnv("PREFIX_SUBCOMMAND_TEST_INT", "456") 130 | assert.NoError(t, err) 131 | 132 | err = lib.BindFlags(cmd, v, envVarPrefix) 133 | assert.NoError(t, err) 134 | 135 | assert.Equal(t, "test-string-value", testString) 136 | assert.Equal(t, 456, testInt) 137 | }) 138 | 139 | t.Run("BindFlags_ArrayFlag", func(t *testing.T) { 140 | assertClearEnv(t) 141 | defer clearEnvVars(t) 142 | 143 | arr := []string{"test", "array", "flag"} 144 | 145 | cmd := &cobra.Command{} 146 | v := getViper() 147 | 148 | var ( 149 | // testArraySpaces []string 150 | testArrayCommas []string 151 | ) 152 | 153 | // cmd.PersistentFlags().StringSliceVar(&testArraySpaces, "test-array-spaces", []string{}, "Test array flag") 154 | cmd.PersistentFlags().StringSliceVar(&testArrayCommas, "test-array-commas", []string{}, "Test array flag") 155 | 156 | // err := setEnv("PREFIX_TEST_ARRAY_SPACES", strings.Join(arr, " ")) 157 | // assert.NoError(t, err) 158 | err := setEnv("PREFIX_TEST_ARRAY_COMMAS", strings.Join(arr, ",")) 159 | assert.NoError(t, err) 160 | 161 | err = lib.BindFlags(cmd, v, envVarPrefix) 162 | assert.NoError(t, err) 163 | 164 | // assert.Equal(t, testArraySpaces, arr) 165 | assert.Equal(t, arr, testArrayCommas) 166 | }) 167 | 168 | t.Run("BindFlags_ReturnsErrorForUnknownConfigurationKeys", func(t *testing.T) { 169 | t.Skip("Not sure if we need this feature.") 170 | assertClearEnv(t) 171 | defer clearEnvVars(t) 172 | 173 | cmd := &cobra.Command{} 174 | v := getViper() 175 | 176 | var ( 177 | testString string 178 | ) 179 | 180 | cmd.PersistentFlags().StringVar(&testString, "test-string", "", "Test string flag") 181 | 182 | v.Set("unknown-key", "unknown-value") 183 | 184 | err := lib.BindFlags(cmd, v, envVarPrefix) 185 | 186 | assert.EqualError(t, err, "unknown configuration key: 'unknown-key'\nShowing help for '' command") 187 | }) 188 | 189 | t.Run("BindFlags_LowerCaseEnvVars", func(t *testing.T) { 190 | assertClearEnv(t) 191 | defer clearEnvVars(t) 192 | 193 | cmd := &cobra.Command{} 194 | v := getViper() 195 | 196 | var ( 197 | testString string 198 | ) 199 | 200 | cmd.PersistentFlags().StringVar(&testString, "test-string", "", "Test string flag") 201 | 202 | err := setEnv("prefix_test_string", "test-string-value") 203 | assert.NoError(t, err) 204 | 205 | err = lib.BindFlags(cmd, v, envVarPrefix) 206 | assert.NoError(t, err) 207 | 208 | assert.Equal(t, "test-string-value", testString) 209 | }) 210 | 211 | t.Run("BindFlags_OneWordFlagName", func(t *testing.T) { 212 | assertClearEnv(t) 213 | defer clearEnvVars(t) 214 | 215 | cmd := &cobra.Command{} 216 | v := getViper() 217 | 218 | var ( 219 | testString string 220 | ) 221 | 222 | cmd.Flags().StringVar(&testString, "teststring", "", "Test string flag") 223 | 224 | err := setEnv("prefix_teststring", "test-string-value") 225 | assert.NoError(t, err) 226 | 227 | err = lib.BindFlags(cmd, v, envVarPrefix) 228 | assert.NoError(t, err) 229 | 230 | assert.Equal(t, "test-string-value", testString) 231 | }) 232 | 233 | t.Run("BindFlags_SameFlagNameDifferentCmd", func(t *testing.T) { 234 | /* 235 | When the same flag name is used in different commands, the last command 236 | will overwrite the previous one. 237 | var ( 238 | cmd1op1 string 239 | cmd1op2 string 240 | rootOp1 string 241 | rootOp2 string 242 | ) 243 | 244 | func Execute() { 245 | var rootCmd = &cobra.Command{ 246 | Use: "", 247 | Run: func(cmd *cobra.Command, args []string) { 248 | log.Printf("cmd1op1: %s", cmd1op1) 249 | log.Printf("cmd1op2: %s", cmd1op2) 250 | log.Printf("rootOp1: %s", rootOp1) 251 | log.Printf("rootOp2: %s", rootOp2) 252 | }, 253 | } 254 | 255 | cmd1 := &cobra.Command{ 256 | Use: "cmd1", 257 | Run: func(cmd *cobra.Command, args []string) { 258 | log.Printf("cmd1op1: %s", cmd1op1) 259 | log.Printf("cmd1op2: %s", cmd1op2) 260 | log.Printf("rootOp1: %s", rootOp1) 261 | log.Printf("rootOp2: %s", rootOp2) 262 | }, 263 | } 264 | cmd1.PersistentFlags().StringVar(&cmd1op1, "op1", "", "persistent option1 for cmd1, not required for rootCmd") 265 | cmd1.Flags().StringVar(&cmd1op2, "op2", "", "option2 for cmd1, not required for rootCmd") 266 | rootCmd.AddCommand(cmd1) 267 | 268 | rootCmd.PersistentFlags().StringVar(&rootOp1, "op1", "", "persistent option1 for rootCmd, not required for cmd1") 269 | rootCmd.Flags().StringVar(&rootOp2, "op2", "", "option2 for rootCmd, not required for cmd1") 270 | 271 | err := rootCmd.Execute() 272 | if err != nil { 273 | os.Exit(1) 274 | } 275 | } 276 | */ 277 | assertClearEnv(t) 278 | defer clearEnvVars(t) 279 | 280 | rootCmd := &cobra.Command{ 281 | Use: "root", 282 | } 283 | cmd1 := &cobra.Command{ 284 | Use: "cmd1", 285 | } 286 | cmd2 := &cobra.Command{ 287 | Use: "cmd2", 288 | } 289 | v := getViper() 290 | 291 | var ( 292 | testStringRoot string 293 | testStringPersistentRoot string 294 | testString1 string 295 | testStringPersistent1 string 296 | testString2 string 297 | testStringPersistent2 string 298 | ) 299 | 300 | rootCmd.Flags().StringVar(&testStringRoot, "test-string", "", "Test string flag") 301 | rootCmd.PersistentFlags().StringVar(&testStringPersistentRoot, "test-string-persistent", "", "Test string flag") 302 | cmd1.Flags().StringVar(&testString1, "test-string", "", "Test string flag") 303 | cmd1.PersistentFlags().StringVar(&testStringPersistent1, "test-string-persistent", "", "Test string flag") 304 | cmd2.Flags().StringVar(&testString2, "test-string", "", "Test string flag") 305 | cmd2.PersistentFlags().StringVar(&testStringPersistent2, "test-string-persistent", "", "Test string flag") 306 | 307 | rootCmd.AddCommand(cmd1) 308 | rootCmd.AddCommand(cmd2) 309 | 310 | err := setEnv("prefix_test_string", "test-string-value") 311 | assert.NoError(t, err) 312 | err = setEnv("prefix_test_string_persistent", "test-string-persistent-value") 313 | assert.NoError(t, err) 314 | err = setEnv("prefix_cmd1_test_string", "test-string-value-cmd1") 315 | assert.NoError(t, err) 316 | err = setEnv("prefix_cmd1_test_string_persistent", "test-string-persistent-value-cmd1") 317 | assert.NoError(t, err) 318 | err = setEnv("prefix_cmd2_test_string", "test-string-value-cmd2") 319 | assert.NoError(t, err) 320 | err = setEnv("prefix_cmd2_test_string_persistent", "test-string-persistent-value-cmd2") 321 | assert.NoError(t, err) 322 | 323 | err = lib.BindFlags(rootCmd, v, envVarPrefix) 324 | assert.NoError(t, err) 325 | 326 | assert.Equal(t, "test-string-value", testStringRoot) 327 | assert.Equal(t, "test-string-persistent-value", testStringPersistentRoot) 328 | assert.Equal(t, "test-string-value-cmd1", testString1) 329 | assert.Equal(t, "test-string-persistent-value-cmd1", testStringPersistent1) 330 | assert.Equal(t, "test-string-value-cmd2", testString2) 331 | assert.Equal(t, "test-string-persistent-value-cmd2", testStringPersistent2) 332 | }) 333 | 334 | t.Run("BindFlags_FromYAML_RootCMD", func(t *testing.T) { 335 | assertClearEnv(t) 336 | defer clearEnvVars(t) 337 | 338 | yamlConfig := []byte(` 339 | test-string: test-string-value 340 | test-int: 123 341 | test-bool: true 342 | test-array: 343 | - test 344 | - array 345 | - flag 346 | test-float: 123.456 347 | `) 348 | 349 | cmd := &cobra.Command{} 350 | v := getViper() 351 | v.SetConfigType("yaml") 352 | assert.NoError(t, v.ReadConfig(bytes.NewBuffer(yamlConfig))) 353 | 354 | var ( 355 | testString string 356 | testInt int 357 | testBool bool 358 | testArray []string 359 | testFloat float64 360 | ) 361 | 362 | cmd.PersistentFlags().StringVar(&testString, "test-string", "", "Test string flag") 363 | cmd.Flags().IntVar(&testInt, "test-int", 0, "Test int flag") 364 | cmd.PersistentFlags().BoolVar(&testBool, "test-bool", false, "Test bool flag") 365 | cmd.Flags().StringSliceVar(&testArray, "test-array", []string{}, "Test array flag") 366 | cmd.PersistentFlags().Float64Var(&testFloat, "test-float", 0, "Test float flag") 367 | 368 | err := lib.BindFlags(cmd, v, envVarPrefix) 369 | assert.NoError(t, err) 370 | 371 | assert.Equal(t, "test-string-value", testString) 372 | assert.Equal(t, 123, testInt) 373 | assert.Equal(t, true, testBool) 374 | assert.Equal(t, []string{"test", "array", "flag"}, testArray) 375 | assert.Equal(t, 123.456, testFloat) 376 | }) 377 | 378 | t.Run("BindFlags_FromYAML_SubCMD", func(t *testing.T) { 379 | assertClearEnv(t) 380 | defer clearEnvVars(t) 381 | 382 | yamlConfig := []byte(` 383 | global-string: global-string-value 384 | subCommand: 385 | test-string: test-string-value 386 | test-int: 123 387 | test-bool: true 388 | `) 389 | 390 | cmd := &cobra.Command{} 391 | v := getViper() 392 | v.SetConfigType("yaml") 393 | assert.NoError(t, v.ReadConfig(bytes.NewBuffer(yamlConfig))) 394 | 395 | var ( 396 | globalString string 397 | testString string 398 | testInt int 399 | testBool bool 400 | ) 401 | 402 | cmd.PersistentFlags().StringVar(&globalString, "global-string", "", "Global string flag") 403 | subCmd := &cobra.Command{ 404 | Use: "subCommand", 405 | } 406 | cmd.AddCommand(subCmd) 407 | subCmd.PersistentFlags().StringVar(&testString, "test-string", "", "Test string flag") 408 | subCmd.Flags().IntVar(&testInt, "test-int", 0, "Test int flag") 409 | subCmd.PersistentFlags().BoolVar(&testBool, "test-bool", false, "Test bool flag") 410 | 411 | err := lib.BindFlags(cmd, v, envVarPrefix) 412 | assert.NoError(t, err) 413 | 414 | assert.Equal(t, "global-string-value", globalString) 415 | assert.Equal(t, "test-string-value", testString) 416 | assert.Equal(t, 123, testInt) 417 | assert.Equal(t, true, testBool) 418 | }) 419 | 420 | t.Run("BindFlags_FromYAML_SubCMD_WithEnvVars", func(t *testing.T) { 421 | assertClearEnv(t) 422 | defer clearEnvVars(t) 423 | 424 | yamlConfig := []byte(` 425 | global-string: global-string-value 426 | subCommand: 427 | test-string: test-string-value 428 | test-int: 123 429 | test-bool: true 430 | `) 431 | cmd := &cobra.Command{} 432 | v := getViper() 433 | v.SetConfigType("yaml") 434 | assert.NoError(t, v.ReadConfig(bytes.NewBuffer(yamlConfig))) 435 | 436 | var ( 437 | globalString string 438 | testString string 439 | testInt int 440 | testBool bool 441 | ) 442 | 443 | cmd.PersistentFlags().StringVar(&globalString, "global-string", "", "Global string flag") 444 | subCmd := &cobra.Command{ 445 | Use: "subCommand", 446 | } 447 | cmd.AddCommand(subCmd) 448 | subCmd.PersistentFlags().StringVar(&testString, "test-string", "", "Test string flag") 449 | subCmd.Flags().IntVar(&testInt, "test-int", 0, "Test int flag") 450 | subCmd.PersistentFlags().BoolVar(&testBool, "test-bool", false, "Test bool flag") 451 | 452 | err := setEnv("PREFIX_GLOBAL_STRING", "global-string-value-from-env") 453 | assert.NoError(t, err) 454 | err = setEnv("PREFIX_SUBCOMMAND_TEST_STRING", "test-string-value-from-env") 455 | assert.NoError(t, err) 456 | 457 | err = lib.BindFlags(cmd, v, envVarPrefix) 458 | assert.NoError(t, err) 459 | 460 | assert.Equal(t, "global-string-value-from-env", globalString) 461 | assert.Equal(t, "test-string-value-from-env", testString) 462 | assert.Equal(t, 123, testInt) 463 | assert.Equal(t, true, testBool) 464 | }) 465 | 466 | t.Run("BindFlags_FromYAML_SubSubCmd", func(t *testing.T) { 467 | assertClearEnv(t) 468 | defer clearEnvVars(t) 469 | 470 | yamlConfig := []byte(` 471 | global-string: global-string-value 472 | subCommand: 473 | first-string: string-from-sub-command 474 | subSubCommand: 475 | second-string: string from sub-sub command 476 | `) 477 | cmd := &cobra.Command{} 478 | v := getViper() 479 | v.SetConfigType("yaml") 480 | assert.NoError(t, v.ReadConfig(bytes.NewBuffer(yamlConfig))) 481 | 482 | var ( 483 | globalString string 484 | firstString string 485 | secondString string 486 | ) 487 | 488 | subSubCmd := &cobra.Command{ 489 | Use: "subSubCommand", 490 | } 491 | subCmd := &cobra.Command{ 492 | Use: "subCommand", 493 | } 494 | subCmd.AddCommand(subSubCmd) 495 | cmd.AddCommand(subCmd) 496 | cmd.PersistentFlags().StringVar(&globalString, "global-string", "", "Global string flag") 497 | subCmd.PersistentFlags().StringVar(&firstString, "first-string", "", "Test string flag") 498 | subSubCmd.Flags().StringVar(&secondString, "second-string", "", "Test string flag") 499 | 500 | err := lib.BindFlags(cmd, v, envVarPrefix) 501 | assert.NoError(t, err) 502 | 503 | assert.Equal(t, "global-string-value", globalString) 504 | assert.Equal(t, "string-from-sub-command", firstString) 505 | assert.Equal(t, "string from sub-sub command", secondString) 506 | }) 507 | 508 | t.Run("BindFlags_FromYAML_SameFlagName_Root", func(t *testing.T) { 509 | assertClearEnv(t) 510 | defer clearEnvVars(t) 511 | 512 | yamlConfig := []byte(` 513 | test-string: global-string-value 514 | subCommand: 515 | dummy-string: string-from-sub-command 516 | `) 517 | 518 | cmd := &cobra.Command{} 519 | v := getViper() 520 | v.SetConfigType("yaml") 521 | assert.NoError(t, v.ReadConfig(bytes.NewBuffer(yamlConfig))) 522 | 523 | var ( 524 | testStringRoot string 525 | testStringSub string 526 | ) 527 | 528 | subCmd := &cobra.Command{ 529 | Use: "subCommand", 530 | } 531 | cmd.AddCommand(subCmd) 532 | 533 | cmd.PersistentFlags().StringVar(&testStringRoot, "test-string", "", "Test string flag") 534 | subCmd.PersistentFlags().StringVar(&testStringSub, "test-string", "", "Test string flag") 535 | 536 | err := lib.BindFlags(cmd, v, envVarPrefix) 537 | assert.NoError(t, err) 538 | 539 | assert.Equal(t, "global-string-value", testStringRoot) 540 | assert.Equal(t, "", testStringSub) 541 | }) 542 | 543 | t.Run("BindFlags_FromYAML_SameFlagName_SubCmd", func(t *testing.T) { 544 | assertClearEnv(t) 545 | defer clearEnvVars(t) 546 | 547 | yamlConfig := []byte(` 548 | test-string: global-string-value 549 | subCommand: 550 | test-string: string-from-sub-command 551 | `) 552 | 553 | cmd := &cobra.Command{} 554 | v := getViper() 555 | v.SetConfigType("yaml") 556 | assert.NoError(t, v.ReadConfig(bytes.NewBuffer(yamlConfig))) 557 | 558 | var ( 559 | testStringRoot string 560 | testStringSub string 561 | ) 562 | 563 | subCmd := &cobra.Command{ 564 | Use: "subCommand", 565 | } 566 | 567 | cmd.PersistentFlags().StringVar(&testStringRoot, "test-string", "", "Test string flag") 568 | subCmd.PersistentFlags().StringVar(&testStringSub, "test-string", "", "Test string flag") 569 | 570 | cmd.AddCommand(subCmd) 571 | 572 | err := lib.BindFlags(cmd, v, envVarPrefix) 573 | assert.NoError(t, err) 574 | 575 | assert.Equal(t, "global-string-value", testStringRoot) 576 | assert.Equal(t, "string-from-sub-command", testStringSub) 577 | }) 578 | 579 | t.Run("BindFlags_FromJSON", func(t *testing.T) { 580 | assertClearEnv(t) 581 | defer clearEnvVars(t) 582 | 583 | jsonConfig := []byte(` 584 | { 585 | "global-string": "global-string-value", 586 | "subCommand": { 587 | "test-string": "string-from-sub-command" 588 | } 589 | }`) 590 | 591 | cmd := &cobra.Command{} 592 | v := getViper() 593 | v.SetConfigType("json") 594 | assert.NoError(t, v.ReadConfig(bytes.NewBuffer(jsonConfig))) 595 | 596 | subCmd := &cobra.Command{ 597 | Use: "subCommand", 598 | } 599 | cmd.AddCommand(subCmd) 600 | 601 | globalString := cmd.PersistentFlags().String("global-string", "", "Global string flag") 602 | testString := subCmd.PersistentFlags().String("test-string", "", "Test string flag") 603 | 604 | err := lib.BindFlags(cmd, v, envVarPrefix) 605 | assert.NoError(t, err) 606 | 607 | assert.Equal(t, "global-string-value", *globalString) 608 | assert.Equal(t, "string-from-sub-command", *testString) 609 | }) 610 | } 611 | 612 | func TestEndToEndWithExecute(t *testing.T) { 613 | configFlagName := "config" 614 | 615 | testCases := []struct { 616 | name string 617 | args []string 618 | envVars map[string]string 619 | config []byte 620 | configFormat string 621 | }{ 622 | { 623 | name: "from env vars", 624 | args: []string{"subcommand"}, 625 | envVars: map[string]string{"TEST_STRING": "env-value", "TEST_INT": "123", "SUBCOMMAND_TEST_BOOL": "true"}, 626 | }, 627 | { 628 | name: "from argument", 629 | args: []string{"subcommand", "--test-string", "argument-value", "--test-int", "123", "--test-bool", "true"}, 630 | }, 631 | { 632 | name: "from config", 633 | args: []string{"subcommand"}, 634 | config: []byte(` 635 | test-string: config-value 636 | test-int: 123 637 | subcommand: 638 | test-bool: true 639 | `), 640 | configFormat: "yaml", 641 | }, 642 | { 643 | name: "from argument and env vars", 644 | args: []string{"subcommand", "--test-string", "argument-value"}, 645 | envVars: map[string]string{ 646 | "TEST_INT": "123", 647 | "SUBCOMMAND_TEST_BOOL": "true", 648 | }, 649 | }, 650 | { 651 | name: "from env vars and config", 652 | args: []string{"subcommand"}, 653 | envVars: map[string]string{ 654 | "TEST_STRING": "env-value", 655 | }, 656 | config: []byte(` 657 | test-int: 123 658 | subcommand: 659 | test-bool: true 660 | `), 661 | configFormat: "yaml", 662 | }, 663 | { 664 | name: "from JSON config", 665 | args: []string{"subcommand"}, 666 | config: []byte(` 667 | { 668 | "test-string": "config-value", 669 | "test-int": 123, 670 | "subcommand": { 671 | "test-bool": true 672 | } 673 | }`), 674 | configFormat: "json", 675 | }, 676 | } 677 | 678 | var cmd *cobra.Command 679 | var v *viper.Viper 680 | 681 | cobra.OnInitialize(func() { 682 | configFilePath, err := cmd.Flags().GetString(configFlagName) 683 | if err != nil { 684 | cobra.CheckErr(err) 685 | } 686 | err = lib.LoadConfig(v, configFilePath) 687 | assert.NoError(t, err) 688 | err = lib.BindFlags(cmd, v, envVarPrefix) 689 | assert.NoError(t, err) 690 | }) 691 | 692 | for _, tc := range testCases { 693 | t.Run(tc.name, func(t *testing.T) { 694 | assertClearEnv(t) 695 | for key, value := range tc.envVars { 696 | err := setEnv(envVarPrefix+"_"+key, value) 697 | assert.NoError(t, err) 698 | } 699 | defer clearEnvVars(t) 700 | 701 | var configFileName string 702 | if tc.config != nil { 703 | configFileName = writeTempFile(t, tc.config, tc.configFormat) 704 | defer os.Remove(configFileName) 705 | 706 | tc.args = append(tc.args, "--"+configFlagName, configFileName) 707 | } 708 | 709 | cmd = &cobra.Command{ 710 | Use: "root", 711 | } 712 | testString := cmd.PersistentFlags().String("test-string", "", "Test string flag") 713 | testInt := cmd.PersistentFlags().Int("test-int", 0, "Test int flag") 714 | assert.NoError(t, cmd.MarkPersistentFlagRequired("test-string")) 715 | cmd.PersistentFlags().String(configFlagName, "", "Config file name") 716 | 717 | var subcommandBool bool 718 | var subCommandExecuted bool 719 | subCmd := &cobra.Command{ 720 | Use: "subcommand", 721 | Run: func(cmd *cobra.Command, args []string) { 722 | assert.NotEmpty(t, *testString) 723 | assert.NotEmpty(t, *testInt) 724 | assert.NotEmpty(t, subcommandBool) 725 | subCommandExecuted = true 726 | }, 727 | } 728 | subCmd.Flags().BoolVar(&subcommandBool, "test-bool", false, "Subcommand string flag") 729 | cmd.AddCommand(subCmd) 730 | 731 | v = getViper() 732 | 733 | cmd.SetArgs(tc.args) 734 | err := cmd.Execute() 735 | assert.NoError(t, err) 736 | 737 | assert.True(t, subCommandExecuted) 738 | subCommandExecuted = false 739 | }) 740 | } 741 | } 742 | 743 | var envKeys []string 744 | 745 | func assertClearEnv(t *testing.T) { 746 | assert.Len(t, envKeys, 0) 747 | } 748 | 749 | func setEnv(key, value string) error { 750 | envKeys = append(envKeys, key) 751 | return os.Setenv(key, value) 752 | } 753 | 754 | func clearEnvVars(t *testing.T) { 755 | for len(envKeys) > 0 { 756 | key := envKeys[0] 757 | err := os.Unsetenv(key) 758 | assert.NoError(t, err) 759 | envKeys = envKeys[1:] 760 | } 761 | } 762 | 763 | func writeTempFile(t *testing.T, content []byte, fileExtension string) string { 764 | file, err := os.CreateTemp("", "config-*."+fileExtension) 765 | assert.NoError(t, err) 766 | 767 | _, err = file.Write([]byte(content)) 768 | assert.NoError(t, err) 769 | assert.NoError(t, file.Close()) 770 | 771 | return file.Name() 772 | } 773 | 774 | func getViper() *viper.Viper { 775 | v := viper.New() 776 | v.SetEnvPrefix(envVarPrefix) 777 | 778 | return v 779 | } 780 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 7 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 8 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 9 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 10 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 11 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 12 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 13 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 14 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 15 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 16 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 17 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= 18 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= 19 | cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= 20 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 21 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 22 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 23 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 24 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 25 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 26 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 27 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 28 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 29 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 30 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 31 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 32 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 33 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 34 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 35 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 36 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 37 | cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= 38 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 39 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 40 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 41 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 42 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 43 | github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY= 44 | github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= 45 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 46 | github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= 47 | github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= 48 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 49 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 50 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 51 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 52 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 53 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 54 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 55 | github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 56 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 57 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 58 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 59 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 60 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 61 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 62 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 63 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 64 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 65 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 66 | github.com/fatih/semgroup v1.2.0 h1:h/OLXwEM+3NNyAdZEpMiH1OzfplU09i2qXPVThGZvyg= 67 | github.com/fatih/semgroup v1.2.0/go.mod h1:1KAD4iIYfXjE4U13B48VM4z9QUwV5Tt8O4rS879kgm8= 68 | github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= 69 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 70 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 71 | github.com/gitleaks/go-gitdiff v0.8.0 h1:7aExTZm+K/M/EQKOyYcub8rIAdWK6ONxPGuRzxmWW+0= 72 | github.com/gitleaks/go-gitdiff v0.8.0/go.mod h1:pKz0X4YzCKZs30BL+weqBIG7mx0jl4tF1uXV9ZyNvrA= 73 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 74 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 75 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 76 | github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 77 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 78 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 79 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 80 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 81 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 82 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 83 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 84 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 85 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 86 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 87 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 88 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 89 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 90 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 91 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 92 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 93 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 94 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 95 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 96 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 97 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 98 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 99 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 100 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 101 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 102 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 103 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 104 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 105 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 106 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 107 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 108 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 109 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 110 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 111 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 112 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 113 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 114 | github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= 115 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 116 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 117 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 118 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 119 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 120 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 121 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 122 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 123 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 124 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 125 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 126 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 127 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 128 | github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 129 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 130 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 131 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 132 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 133 | github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= 134 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 135 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 136 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 137 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 138 | github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= 139 | github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= 140 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 141 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 142 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 143 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 144 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 145 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 146 | github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 147 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 148 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 149 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 150 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 151 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 152 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 153 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 154 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 155 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 156 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 157 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 158 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 159 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 160 | github.com/lucasjones/reggen v0.0.0-20200904144131-37ba4fa293bb h1:w1g9wNDIE/pHSTmAaUhv4TZQuPBS6GV3mMz5hkgziIU= 161 | github.com/lucasjones/reggen v0.0.0-20200904144131-37ba4fa293bb/go.mod h1:5ELEyG+X8f+meRWHuqUOewBOhvHkl7M76pdGEansxW4= 162 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 163 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 164 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 165 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 166 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 167 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 168 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 169 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 170 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 171 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 172 | github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= 173 | github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 174 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 175 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 176 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= 177 | github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= 178 | github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= 179 | github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= 180 | github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us= 181 | github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= 182 | github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9 h1:lL+y4Xv20pVlCGyLzNHRC0I0rIHhIL1lTvHizoS/dU8= 183 | github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw= 184 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 185 | github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 186 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 187 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 188 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 189 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 190 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 191 | github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= 192 | github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 193 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 194 | github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= 195 | github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 196 | github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= 197 | github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= 198 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 199 | github.com/slack-go/slack v0.12.2 h1:x3OppyMyGIbbiyFhsBmpf9pwkUzMhthJMRNmNlA4LaQ= 200 | github.com/slack-go/slack v0.12.2/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= 201 | github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= 202 | github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= 203 | github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= 204 | github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= 205 | github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= 206 | github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= 207 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= 208 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 209 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 210 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 211 | github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= 212 | github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA= 213 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 214 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 215 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 216 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 217 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 218 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 219 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 220 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 221 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 222 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 223 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 224 | github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= 225 | github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= 226 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 227 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 228 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 229 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 230 | github.com/zricethezav/gitleaks/v8 v8.16.1 h1:Y+x4GNIZx70fPQBJseuZydecadlgbKUJgirNeySSZw8= 231 | github.com/zricethezav/gitleaks/v8 v8.16.1/go.mod h1:JzsbTtA88h1ioImguqG0BL2IV2JfblVO0qj/LSwgUKQ= 232 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 233 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 234 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 235 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 236 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 237 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 238 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 239 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 240 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 241 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 242 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 243 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 244 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= 245 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 246 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 247 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 248 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 249 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 250 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 251 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 252 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 253 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 254 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 255 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 256 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 257 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 258 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 259 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 260 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 261 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 262 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 263 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 264 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 265 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 266 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 267 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 268 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 269 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 270 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 271 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 272 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 273 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 274 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 275 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 276 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 277 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 278 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 279 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 280 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 281 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 282 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 283 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 284 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 285 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 286 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 287 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 288 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 289 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 290 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 291 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 292 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 293 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 294 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 295 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 296 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 297 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 298 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 299 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 300 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 301 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 302 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 303 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 304 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 305 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 306 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 307 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 308 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 309 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 310 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 311 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 312 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 313 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 314 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 315 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 316 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 317 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 318 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 319 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 320 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 321 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 322 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 323 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 324 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 325 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 326 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 327 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 328 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 329 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 330 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 331 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 332 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 333 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 334 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 335 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 336 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 337 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 338 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 339 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 340 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 341 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 342 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 343 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 344 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 345 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 346 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 347 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 348 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 349 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 350 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 351 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 352 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 353 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 354 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 355 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 356 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 357 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 358 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 359 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 360 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 361 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 362 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 363 | golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 364 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 365 | golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 366 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 367 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 368 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 369 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 370 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 371 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 372 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 373 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 374 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 375 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 376 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 377 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 378 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 379 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 380 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 381 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 382 | golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= 383 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 384 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 385 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 386 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 387 | golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= 388 | golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 389 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 390 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 391 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 392 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 393 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 394 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 395 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 396 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 397 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 398 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 399 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 400 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 401 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 402 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 403 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 404 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 405 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 406 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 407 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 408 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 409 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 410 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 411 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 412 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 413 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 414 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 415 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 416 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 417 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 418 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 419 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 420 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 421 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 422 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 423 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 424 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 425 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 426 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 427 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 428 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 429 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= 430 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 431 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 432 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 433 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 434 | golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 435 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 436 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 437 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 438 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 439 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 440 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 441 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 442 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 443 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 444 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 445 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 446 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 447 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 448 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 449 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 450 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 451 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 452 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 453 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 454 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 455 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 456 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= 457 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= 458 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= 459 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 460 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 461 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 462 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 463 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 464 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 465 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 466 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 467 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 468 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 469 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 470 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 471 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 472 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 473 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 474 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 475 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 476 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 477 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 478 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 479 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 480 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 481 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 482 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 483 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 484 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 485 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 486 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 487 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 488 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 489 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 490 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 491 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 492 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 493 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 494 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 495 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 496 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 497 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 498 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 499 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 500 | google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 501 | google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 502 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 503 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 504 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 505 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 506 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 507 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 508 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 509 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 510 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 511 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 512 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 513 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 514 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 515 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 516 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= 517 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 518 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 519 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 520 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 521 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 522 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 523 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 524 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 525 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 526 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 527 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 528 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 529 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 530 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 531 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 532 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 533 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 534 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 535 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 536 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 537 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 538 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 539 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 540 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 541 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 542 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 543 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 544 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 545 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 546 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 547 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 548 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 549 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 550 | --------------------------------------------------------------------------------