├── .dockerignore ├── .gitignore ├── .goreleaser.yml ├── .travis.yml ├── Dockerfile ├── LICENSE.txt ├── Makefile ├── README.md ├── cmd ├── addcredential.go ├── discover.go ├── dupes.go ├── host.go ├── logcheck.go ├── report.go ├── rescan.go ├── root.go ├── scan.go ├── version.go └── vuln.go ├── demo ├── README.md ├── intro.json └── intro.yaml ├── docker-compose.yml ├── go.mod ├── go.sum ├── main.go ├── sshauditor ├── auditor.go ├── auditor_test.go ├── banner.go ├── bannerworker.go ├── batch.go ├── batch_test.go ├── brute.go ├── logsearch.go ├── netutil.go ├── netutil_test.go ├── prompt.go ├── sshutil.go ├── sshutil_test.go ├── sshworker.go ├── store.go └── store_test.go └── testing └── docker ├── alpine-sshd-ok └── Dockerfile ├── alpine-sshd-test-blank └── Dockerfile ├── alpine-sshd-test-key ├── Dockerfile ├── test.key └── test.pub ├── alpine-sshd-test-test-no-id-binary-no-tunnel └── Dockerfile ├── alpine-sshd-test-test-no-id-binary-tunnel-local ├── Dockerfile └── sshd_config ├── alpine-sshd-test-test-no-id-binary ├── Dockerfile └── sshd_config └── alpine-sshd-test-test └── Dockerfile /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | testing/docker/*/Dockerfile 3 | testing/docker/*/sshd_config 4 | *.sqlite 5 | ssh-auditor 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite 2 | ssh-auditor 3 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: ssh-auditor 2 | release: 3 | github: 4 | owner: ncsa 5 | name: ssh-auditor 6 | name_template: '{{.Tag}}' 7 | builds: 8 | - goos: 9 | - linux 10 | goarch: 11 | - amd64 12 | goarm: 13 | - "6" 14 | main: . 15 | flags: -trimpath 16 | ldflags: -s -w -X github.com/ncsa/ssh-auditor/cmd.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -extldflags "-static" 17 | binary: ssh-auditor 18 | archives: 19 | - 20 | wrap_in_directory: true 21 | format: tar.gz 22 | name_template: '{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ 23 | .Arm }}{{ end }}' 24 | files: 25 | - licence* 26 | - LICENCE* 27 | - license* 28 | - LICENSE* 29 | - readme* 30 | - README* 31 | - changelog* 32 | - CHANGELOG* 33 | snapshot: 34 | name_template: SNAPSHOT-{{ .Commit }} 35 | checksum: 36 | name_template: '{{ .ProjectName }}_{{ .Version }}_checksums.txt' 37 | nfpms: 38 | - 39 | description: scan for weak ssh passwords on your network 40 | license: University of Illinois/NCSA Open Source License (NCSA) 41 | file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' 42 | bindir: /usr/bin 43 | homepage: https://github.com/ncsa/ssh-auditor 44 | maintainer: Justin Azoff 45 | formats: 46 | - deb 47 | - rpm 48 | env_files: 49 | github_token: ~/.config/goreleaser/github_token 50 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: go 3 | addons: 4 | apt: 5 | packages: 6 | # needed for the nfpm pipe: 7 | - rpm 8 | go: 9 | - "1.15" 10 | - "1.16" 11 | 12 | services: 13 | - docker 14 | 15 | script: 16 | - make all 17 | - make e2e-test 18 | 19 | # calls goreleaser 20 | deploy: 21 | - provider: script 22 | skip_cleanup: true 23 | script: curl -sL https://git.io/goreleaser | bash 24 | on: 25 | tags: true 26 | condition: $TRAVIS_OS_NAME = linux 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.13 2 | 3 | MAINTAINER Justin Azoff 4 | 5 | RUN mkdir /src 6 | WORKDIR /src 7 | COPY go.mod . 8 | COPY go.sum . 9 | # Get dependancies - will also be cached if we won't change mod/sum 10 | RUN go mod download 11 | 12 | ADD . /src/ 13 | 14 | RUN go get 15 | RUN go build 16 | 17 | CMD ["/bin/sh"] 18 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 University of Illinois/NCSA 2 | All rights reserved. 3 | 4 | Developed by: NCSA Cyber Security 5 | NCSA 6 | www.ncsa.illinois.edu 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of 9 | this software and associated documentation files (the "Software"), to deal with 10 | the Software without restriction, including without limitation the rights to 11 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 12 | of the Software, and to permit persons to whom the Software is furnished to do 13 | so, subject to the following conditions: 14 | 15 | * Redistributions of source code must retain the above copyright notice, this 16 | list of conditions and the following disclaimers. 17 | 18 | * Redistributions in binary form must reproduce the above copyright notice, 19 | this list of conditions and the following disclaimers in the documentation 20 | and/or other materials provided with the distribution. 21 | 22 | * Neither the names of , nor 23 | the names of its contributors may be used to endorse or promote products 24 | derived from this Software without specific prior written permission. 25 | 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE 32 | SOFTWARE. 33 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: build test 2 | build: 3 | go build 4 | test: 5 | go test -v -short ./... 6 | e2e-test: 7 | docker-compose up --abort-on-container-exit --build 8 | static: 9 | go build --ldflags '-extldflags "-static"' 10 | 11 | .PHONY: rpm 12 | rpm: 13 | goreleaser --skip-publish 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/ncsa/ssh-auditor.svg?branch=master)](https://travis-ci.org/ncsa/ssh-auditor) 2 | 3 | # SSH Auditor 4 | 5 | 6 | ## Features 7 | 8 | ssh-auditor will automatically: 9 | 10 | * Re-check all known hosts as new credentials are added. It will only check the new credentials. 11 | * Queue a full credential scan on any new host discovered. 12 | * Queue a full credential scan on any known host whose ssh version or key fingerprint changes. 13 | * Attempt command execution as well as attempt to tunnel a TCP connection. 14 | * Re-check each credential using a per credential `scan_interval` - default 14 days. 15 | 16 | 17 | It's designed so that you can run `ssh-auditor discover` + `ssh-auditor scan` 18 | from cron every hour to to perform a constant audit. 19 | 20 | ## Demos 21 | 22 | # Earlier demo showing all of the features 23 | [![demo](https://asciinema.org/a/5rb3wv8oyoqzd80jfl03grrcv.png)](https://asciinema.org/a/5rb3wv8oyoqzd80jfl03grrcv?autoplay=1) 24 | 25 | # Demo showing improved log output 26 | 27 | [![demo](https://asciinema.org/a/F3fQYyJcieCS9Kfna6xWferjK.png)](https://asciinema.org/a/F3fQYyJcieCS9Kfna6xWferjK?autoplay=1) 28 | 29 | 30 | ## Usage 31 | 32 | ### Install 33 | 34 | $ brew install go # or however you want to install the go compiler 35 | $ go get github.com/ncsa/ssh-auditor 36 | 37 | ### or Build from a git clone 38 | 39 | $ go build 40 | 41 | ### Build a static binary including sqlite 42 | 43 | $ make static 44 | 45 | ### Ensure you can use enough file descriptors 46 | 47 | $ ulimit -n 4096 48 | 49 | ### Create initial database and discover ssh servers 50 | 51 | $ ./ssh-auditor discover -p 22 -p 2222 192.168.1.0/24 10.0.0.1/24 52 | 53 | ### Add credential pairs to check 54 | 55 | $ ./ssh-auditor addcredential root root 56 | $ ./ssh-auditor addcredential admin admin 57 | $ ./ssh-auditor addcredential guest guest --scan-interval 1 #check this once per day 58 | 59 | ### Try credentials against discovered hosts 60 | 61 | $ ./ssh-auditor scan 62 | 63 | ### Output a report on what credentials worked 64 | 65 | $ ./ssh-auditor vuln 66 | 67 | ### RE-Check credentials that worked 68 | 69 | $ ./ssh-auditor rescan 70 | 71 | ### Output a report on duplicate key usage 72 | 73 | $ ./ssh-auditor dupes 74 | 75 | ## TODO 76 | 77 | - [x] update the 'host changes' table 78 | - [x] handle false positives from devices that don't use ssh password authentication but instead use the shell to do it. 79 | - [x] variable re-check times - each credential has a scan_interval in days 80 | - [x] better support non-standard ports - discover is the only thing that needs to be updated, the rest doesn't care. 81 | - [ ] possibly daemonize and add an api that bro could hook into to kick off a discover as soon as a new SSH server is detected. 82 | - [ ] make the store pluggable (mysql, postgresql). 83 | - [x] differentiate between a failed password attempt and a failed connection or timeout. Mostly done. Things like fail2ban complicate this. 84 | - [x] add go implementations for the report sqlite3 command. 85 | 86 | ## Report query. 87 | 88 | This query that `ssh-auditor vuln` runs is 89 | 90 | select 91 | hc.hostport, hc.user, hc.password, hc.result, hc.last_tested, h.version 92 | from 93 | host_creds hc, hosts h 94 | where 95 | h.hostport = hc.hostport 96 | and result!='' order by last_tested asc 97 | -------------------------------------------------------------------------------- /cmd/addcredential.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bufio" 5 | "encoding/csv" 6 | "encoding/json" 7 | "os" 8 | "strconv" 9 | 10 | log "github.com/inconshreveable/log15" 11 | 12 | "github.com/ncsa/ssh-auditor/sshauditor" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | var credentialCmd = &cobra.Command{ 17 | Use: "credential", 18 | Short: "manage credentials", 19 | Aliases: []string{"cred", "c"}, 20 | } 21 | 22 | var scanIntervalDays int 23 | 24 | var credentialAddCmd = &cobra.Command{ 25 | Use: "add", 26 | Aliases: []string{"addcredential", "ac", "add"}, 27 | Short: "add a new credential pair", 28 | Example: "add root root123", 29 | Run: func(cmd *cobra.Command, args []string) { 30 | if len(args) != 2 { 31 | cmd.Usage() 32 | return 33 | } 34 | cred := sshauditor.Credential{ 35 | User: args[0], 36 | Password: args[1], 37 | ScanInterval: scanIntervalDays, 38 | } 39 | l := log.New("user", cred.User, "password", cred.Password, "interval", scanIntervalDays) 40 | added, err := store.AddCredential(cred) 41 | if err != nil { 42 | log.Error(err.Error()) 43 | os.Exit(1) 44 | } 45 | if added { 46 | l.Info("added credential") 47 | } else { 48 | l.Info("updated credential") 49 | } 50 | }, 51 | } 52 | var credentialListCmd = &cobra.Command{ 53 | Use: "list", 54 | Aliases: []string{"l"}, 55 | Short: "list credentials", 56 | Run: func(cmd *cobra.Command, args []string) { 57 | creds, err := store.GetAllCreds() 58 | if err != nil { 59 | log.Error(err.Error()) 60 | os.Exit(1) 61 | } 62 | w := json.NewEncoder(os.Stdout) 63 | for _, c := range creds { 64 | if err := w.Encode(c); err != nil { 65 | panic(err) 66 | } 67 | } 68 | }, 69 | } 70 | 71 | var credentialResetCmd = &cobra.Command{ 72 | Use: "reset", 73 | Aliases: []string{"c"}, 74 | Short: "reset credential list", 75 | Run: func(cmd *cobra.Command, args []string) { 76 | err := store.ResetCreds() 77 | if err != nil { 78 | log.Error(err.Error()) 79 | os.Exit(1) 80 | } 81 | }, 82 | } 83 | 84 | var credentialImportCmd = &cobra.Command{ 85 | Use: "import", 86 | Short: "load credentials from TSV or JSON", 87 | } 88 | 89 | var credentialImportTSVCmd = &cobra.Command{ 90 | Use: "tsv", 91 | Short: "load credentials from TSV", 92 | Long: `Load credentials from stdin in the format of 93 | user password scaninterval 94 | 95 | or 96 | user password 97 | 98 | example: 99 | 100 | root root 7 101 | test test 102 | `, 103 | Run: func(cmd *cobra.Command, args []string) { 104 | reader := csv.NewReader(os.Stdin) 105 | reader.Comma = '\t' 106 | records, err := reader.ReadAll() 107 | if err != nil { 108 | log.Error(err.Error()) 109 | os.Exit(1) 110 | } 111 | store.Begin() 112 | defer store.Commit() 113 | 114 | for _, r := range records { 115 | var si int 116 | if len(r) != 2 && len(r) != 3 { 117 | log.Error("Invalid record", "rec", r) 118 | continue 119 | } 120 | if len(r) == 3 { 121 | si, err = strconv.Atoi(r[2]) 122 | if err != nil { 123 | log.Error("Invalid record", "rec", r, "err", err) 124 | continue 125 | } 126 | } else { 127 | si = scanIntervalDays 128 | } 129 | 130 | cred := sshauditor.Credential{ 131 | User: r[0], 132 | Password: r[1], 133 | ScanInterval: si, 134 | } 135 | l := log.New("user", cred.User, "password", cred.Password, "interval", si) 136 | added, err := store.AddCredential(cred) 137 | if err != nil { 138 | log.Error(err.Error()) 139 | os.Exit(1) 140 | } 141 | if added { 142 | l.Info("added credential") 143 | } else { 144 | l.Info("updated credential") 145 | } 146 | } 147 | }, 148 | } 149 | var credentialImportJSONCmd = &cobra.Command{ 150 | Use: "json", 151 | Short: "load credentials from JSON", 152 | Long: `Load credentials from stdin in the format of 153 | {"User":"root","Password":"root","ScanInterval":7} 154 | {"User":"test","Password":"test"} 155 | `, 156 | Run: func(cmd *cobra.Command, args []string) { 157 | store.Begin() 158 | defer store.Commit() 159 | scanner := bufio.NewScanner(os.Stdin) 160 | for scanner.Scan() { 161 | var cred sshauditor.Credential 162 | json.Unmarshal(scanner.Bytes(), &cred) 163 | if cred.ScanInterval == 0 { 164 | cred.ScanInterval = scanIntervalDays 165 | } 166 | l := log.New("user", cred.User, "password", cred.Password, "interval", cred.ScanInterval) 167 | added, err := store.AddCredential(cred) 168 | if err != nil { 169 | log.Error(err.Error()) 170 | os.Exit(1) 171 | } 172 | if added { 173 | l.Info("added credential") 174 | } else { 175 | l.Info("updated credential") 176 | } 177 | } 178 | if err := scanner.Err(); err != nil { 179 | log.Error("error reading standard input", "err", err) 180 | } 181 | }, 182 | } 183 | 184 | func init() { 185 | credentialAddCmd.Flags().IntVar(&scanIntervalDays, "scan-interval", 14, "How often to re-scan for this credential, in days") 186 | RootCmd.AddCommand(credentialAddCmd) 187 | RootCmd.AddCommand(credentialCmd) 188 | credentialCmd.AddCommand(credentialAddCmd) 189 | credentialCmd.AddCommand(credentialListCmd) 190 | credentialCmd.AddCommand(credentialResetCmd) 191 | credentialCmd.AddCommand(credentialImportCmd) 192 | credentialImportCmd.AddCommand(credentialImportTSVCmd) 193 | credentialImportCmd.AddCommand(credentialImportJSONCmd) 194 | } 195 | -------------------------------------------------------------------------------- /cmd/discover.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | 7 | log "github.com/inconshreveable/log15" 8 | "github.com/ncsa/ssh-auditor/sshauditor" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var ports []int 13 | var exclude []string 14 | 15 | var discoverCmd = &cobra.Command{ 16 | Use: "discover", 17 | Aliases: []string{"d"}, 18 | Example: "discover -p 22 -p 2222 192.168.1.0/24 10.1.1.0/24 --exclude 192.168.1.100/32", 19 | Short: "discover new hosts", 20 | Run: func(cmd *cobra.Command, args []string) { 21 | if len(args) == 0 { 22 | cmd.Usage() 23 | return 24 | } 25 | scanConfig := sshauditor.ScanConfiguration{ 26 | Concurrency: concurrency, 27 | Include: args, 28 | Exclude: exclude, 29 | Ports: ports, 30 | } 31 | auditor := sshauditor.New(store) 32 | err := auditor.Discover(scanConfig) 33 | if err != nil { 34 | log.Error(err.Error()) 35 | os.Exit(1) 36 | } 37 | }, 38 | } 39 | 40 | var discoverFromFileCmd = &cobra.Command{ 41 | Use: "fromfile", 42 | Example: "fromfile -p 22 hosts.txt", 43 | Short: "discover new hosts using a list of hosts from stdin", 44 | Run: func(cmd *cobra.Command, args []string) { 45 | scanner := bufio.NewScanner(os.Stdin) 46 | scanConfig := sshauditor.ScanConfiguration{ 47 | Concurrency: concurrency, 48 | Include: []string{}, 49 | Ports: ports, 50 | } 51 | for scanner.Scan() { 52 | host := scanner.Text() 53 | scanConfig.Include = append(scanConfig.Include, host) 54 | } 55 | auditor := sshauditor.New(store) 56 | err := auditor.Discover(scanConfig) 57 | if err != nil { 58 | log.Error(err.Error()) 59 | os.Exit(1) 60 | } 61 | }, 62 | } 63 | 64 | func init() { 65 | discoverCmd.Flags().IntSliceVarP(&ports, "ports", "p", []int{22}, "ports to check during initial discovery") 66 | discoverCmd.Flags().StringSliceVarP(&exclude, "exclude", "x", []string{}, "subnets to exclude from discovery") 67 | 68 | discoverFromFileCmd.Flags().IntSliceVarP(&ports, "ports", "p", []int{22}, "ports to check during initial discovery") 69 | RootCmd.AddCommand(discoverCmd) 70 | discoverCmd.AddCommand(discoverFromFileCmd) 71 | } 72 | -------------------------------------------------------------------------------- /cmd/dupes.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | 7 | log "github.com/inconshreveable/log15" 8 | "github.com/ncsa/ssh-auditor/sshauditor" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var dupesCmd = &cobra.Command{ 13 | Use: "dupes", 14 | Short: "Show hosts using the same key", 15 | Run: func(cmd *cobra.Command, args []string) { 16 | auditor := sshauditor.New(store) 17 | keyMap, err := auditor.Dupes() 18 | if err != nil { 19 | log.Error(err.Error()) 20 | os.Exit(1) 21 | } 22 | w := json.NewEncoder(os.Stdout) 23 | w.SetIndent("", " ") 24 | err = w.Encode(keyMap) 25 | if err != nil { 26 | log.Error(err.Error()) 27 | os.Exit(1) 28 | } 29 | return 30 | }, 31 | } 32 | 33 | func init() { 34 | RootCmd.AddCommand(dupesCmd) 35 | } 36 | -------------------------------------------------------------------------------- /cmd/host.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | 7 | log "github.com/inconshreveable/log15" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var hostCmd = &cobra.Command{ 13 | Use: "host", 14 | Short: "manage hosts", 15 | Aliases: []string{"host", "h"}, 16 | } 17 | 18 | var hostMaxAgeDays int 19 | 20 | var hostListCmd = &cobra.Command{ 21 | Use: "list", 22 | Aliases: []string{"l"}, 23 | Short: "list hosts", 24 | Run: func(cmd *cobra.Command, args []string) { 25 | hosts, err := store.GetActiveHosts(hostMaxAgeDays) 26 | if err != nil { 27 | log.Error(err.Error()) 28 | os.Exit(1) 29 | } 30 | w := json.NewEncoder(os.Stdout) 31 | for _, c := range hosts { 32 | if err := w.Encode(c); err != nil { 33 | panic(err) 34 | } 35 | } 36 | }, 37 | } 38 | 39 | var hostDeleteCmd = &cobra.Command{ 40 | Use: "delete", 41 | Aliases: []string{"r"}, 42 | Short: "delete hosts", 43 | Run: func(cmd *cobra.Command, args []string) { 44 | for _, host := range args { 45 | err := store.DeleteHost(host) 46 | if err != nil { 47 | log.Error(err.Error()) 48 | os.Exit(1) 49 | } 50 | } 51 | }, 52 | } 53 | 54 | func init() { 55 | RootCmd.AddCommand(hostCmd) 56 | hostCmd.AddCommand(hostListCmd) 57 | hostListCmd.Flags().IntVar(&hostMaxAgeDays, "max-age-days", 14, "List hosts seen at most this many days ago") 58 | hostCmd.AddCommand(hostDeleteCmd) 59 | } 60 | -------------------------------------------------------------------------------- /cmd/logcheck.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | log "github.com/inconshreveable/log15" 7 | "github.com/ncsa/ssh-auditor/sshauditor" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var logcheckCmd = &cobra.Command{ 12 | Use: "logcheck", 13 | Short: "trigger and report on failed ssh authentication attempts", 14 | Aliases: []string{"lc"}, 15 | } 16 | 17 | var logcheckRunCmd = &cobra.Command{ 18 | Use: "run", 19 | Short: "trigger failed ssh authentication attempts", 20 | Long: `trigger failed ssh authentication attempts in order to verify that 21 | local servers are properly shipping logs to a central collector`, 22 | Run: func(cmd *cobra.Command, args []string) { 23 | auditor := sshauditor.New(store) 24 | scanConfig := sshauditor.ScanConfiguration{ 25 | Concurrency: concurrency, 26 | } 27 | err := auditor.Logcheck(scanConfig) 28 | if err != nil { 29 | log.Error(err.Error()) 30 | os.Exit(1) 31 | } 32 | }, 33 | } 34 | 35 | var splunkHost string 36 | 37 | var logcheckReportCmd = &cobra.Command{ 38 | Use: "report", 39 | Aliases: []string{"lc"}, 40 | Short: "compare syslog data to the store", 41 | Long: `After running logcheck, search syslog for failed login attempts in 42 | order to determine which hosts are properly logging to syslog`, 43 | Run: func(cmd *cobra.Command, args []string) { 44 | var ls sshauditor.LogSearcher 45 | if splunkHost != "" { 46 | ls = sshauditor.NewSplunkLogSearcher(splunkHost) 47 | } else { 48 | log.Error("only --splunk supported for now") 49 | os.Exit(1) 50 | } 51 | auditor := sshauditor.New(store) 52 | err := auditor.LogcheckReport(ls) 53 | if err != nil { 54 | log.Error(err.Error()) 55 | os.Exit(1) 56 | } 57 | }, 58 | } 59 | 60 | func init() { 61 | RootCmd.AddCommand(logcheckCmd) 62 | 63 | logcheckCmd.AddCommand(logcheckRunCmd) 64 | 65 | logcheckReportCmd.Flags().StringVar(&splunkHost, "splunk", "", "base url to splunk API (https://host:port)") 66 | logcheckCmd.AddCommand(logcheckReportCmd) 67 | } 68 | -------------------------------------------------------------------------------- /cmd/report.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | html_template "html/template" 6 | "os" 7 | text_template "text/template" 8 | 9 | log "github.com/inconshreveable/log15" 10 | "github.com/ncsa/ssh-auditor/sshauditor" 11 | 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | var reportCmd = &cobra.Command{ 16 | Use: "report", 17 | Short: "output a full audit report", 18 | Aliases: []string{"rep"}, 19 | } 20 | 21 | var reportJSONCmd = &cobra.Command{ 22 | Use: "json", 23 | Short: "json report", 24 | Run: func(cmd *cobra.Command, args []string) { 25 | auditor := sshauditor.New(store) 26 | report, err := auditor.GetReport() 27 | if err != nil { 28 | log.Error(err.Error()) 29 | os.Exit(1) 30 | } 31 | w := json.NewEncoder(os.Stdout) 32 | w.SetIndent("", " ") 33 | err = w.Encode(report) 34 | if err != nil { 35 | log.Error(err.Error()) 36 | os.Exit(1) 37 | } 38 | }, 39 | } 40 | 41 | var reportTXTCmd = &cobra.Command{ 42 | Use: "txt", 43 | Short: "plain text report", 44 | Run: func(cmd *cobra.Command, args []string) { 45 | auditor := sshauditor.New(store) 46 | report, err := auditor.GetReport() 47 | if err != nil { 48 | log.Error(err.Error()) 49 | os.Exit(1) 50 | } 51 | t := text_template.Must(text_template.New("report").Parse(reportTXTTemplate)) 52 | err = t.Execute(os.Stdout, report) 53 | if err != nil { 54 | log.Error(err.Error()) 55 | os.Exit(1) 56 | } 57 | return 58 | }, 59 | } 60 | var reportHTMLCmd = &cobra.Command{ 61 | Use: "html", 62 | Short: "html report", 63 | Run: func(cmd *cobra.Command, args []string) { 64 | auditor := sshauditor.New(store) 65 | report, err := auditor.GetReport() 66 | if err != nil { 67 | log.Error(err.Error()) 68 | os.Exit(1) 69 | } 70 | t := html_template.Must(html_template.New("report").Parse(reportHTMLTemplate)) 71 | err = t.Execute(os.Stdout, report) 72 | if err != nil { 73 | log.Error(err.Error()) 74 | os.Exit(1) 75 | } 76 | return 77 | }, 78 | } 79 | 80 | func init() { 81 | RootCmd.AddCommand(reportCmd) 82 | reportCmd.AddCommand(reportJSONCmd) 83 | reportCmd.AddCommand(reportTXTCmd) 84 | reportCmd.AddCommand(reportHTMLCmd) 85 | } 86 | 87 | var reportTXTTemplate = ` 88 | Vulnerabilities: {{ .VulnerabilitiesCount }} 89 | {{range .Vulnerabilities}} 90 | Host {{.Host.Hostport}} 91 | Version {{.Host.Version}} 92 | User {{.HostCredential.User}} 93 | Password {{.HostCredential.Password}} 94 | Result {{.HostCredential.Result}} 95 | Last Tested {{.HostCredential.LastTested}} 96 | {{end}} 97 | 98 | Duplicate Keys: {{ .DuplicateKeysCount }} 99 | {{ range $key, $hosts := .DuplicateKeys }} 100 | {{$key}}: 101 | {{ range $hosts }} 102 | Host {{.Hostport}} 103 | Version {{.Version}} 104 | Seen First {{.SeenFirst}} 105 | Seen Last {{.SeenLast}} 106 | {{end}} 107 | {{end}} 108 | 109 | Active Hosts: {{ .ActiveHostsCount }} 110 | {{ range .ActiveHosts }} 111 | Host {{.Hostport}} 112 | Version {{.Version}} 113 | Seen First {{.SeenFirst}} 114 | Seen Last {{.SeenLast}} 115 | {{end}} 116 | ` 117 | 118 | var reportHTMLTemplate = ` 119 | 120 | 121 | 122 |

Vulnerabilities: {{ .VulnerabilitiesCount }}

123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | {{range .Vulnerabilities}} 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | {{end}} 145 | 146 |
HostUserPasswordResultLast TestedVersion
{{.Host.Hostport}} {{.HostCredential.User}} {{.HostCredential.Password}} {{.HostCredential.Result}} {{.HostCredential.LastTested}} {{.Host.Version}}
147 | 148 |

Duplicate Keys: {{ .DuplicateKeysCount }}

149 | {{ range $key, $hosts := .DuplicateKeys }} 150 |

{{$key}}

151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | {{ range $hosts }} 162 | 163 | 164 | 165 | 166 | 167 | 168 | {{end}} 169 | 170 |
HostVersionSeen FirstSeen Last
{{.Hostport}} {{.Version}} {{.SeenFirst}} {{.SeenLast}}
171 | {{end}} 172 | 173 |

Active Hosts: {{ .ActiveHostsCount }}

174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | {{ range .ActiveHosts }} 185 | 186 | 187 | 188 | 189 | 190 | 191 | {{end}} 192 | 193 |
HostVersionSeen FirstSeen Last
{{.Hostport}} {{.Version}} {{.SeenFirst}} {{.SeenLast}}
194 | ` 195 | -------------------------------------------------------------------------------- /cmd/rescan.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | log "github.com/inconshreveable/log15" 7 | "github.com/ncsa/ssh-auditor/sshauditor" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var rescanCmd = &cobra.Command{ 12 | Use: "rescan", 13 | Short: "Rescan hosts with credentials that have previously worked", 14 | Run: func(cmd *cobra.Command, args []string) { 15 | scanConfig := sshauditor.ScanConfiguration{ 16 | Concurrency: concurrency, 17 | } 18 | auditor := sshauditor.New(store) 19 | _, err := auditor.Rescan(scanConfig) 20 | if err != nil { 21 | log.Error(err.Error()) 22 | os.Exit(1) 23 | } 24 | }, 25 | } 26 | 27 | func init() { 28 | RootCmd.AddCommand(rescanCmd) 29 | } 30 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | log "github.com/inconshreveable/log15" 7 | "github.com/ncsa/ssh-auditor/sshauditor" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var store *sshauditor.SQLiteStore 12 | var dbPath string 13 | var debug bool 14 | var concurrency int 15 | 16 | func initStore() error { 17 | //This should really return err, but it doesn't look as nice as when I fail immediately 18 | //cobra gives the help for the current command, which is irrelevant 19 | s, err := sshauditor.NewSQLiteStore(dbPath) 20 | if err != nil { 21 | log.Error(err.Error()) 22 | os.Exit(1) 23 | } 24 | err = s.Init() 25 | if err != nil { 26 | log.Error(err.Error()) 27 | os.Exit(1) 28 | } 29 | store = s 30 | return err 31 | } 32 | 33 | var RootCmd = &cobra.Command{ 34 | Use: "ssh-auditor", 35 | Short: "ssh-auditor tests ssh server password security", 36 | Long: `Complete documentation is available at https://github.com/ncsa/ssh-auditor`, 37 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 38 | if debug { 39 | log.Root().SetHandler(log.LvlFilterHandler( 40 | log.LvlDebug, 41 | log.StderrHandler)) 42 | } else { 43 | log.Root().SetHandler(log.LvlFilterHandler( 44 | log.LvlInfo, 45 | log.StderrHandler)) 46 | } 47 | return initStore() 48 | }, 49 | } 50 | 51 | func init() { 52 | RootCmd.PersistentFlags().IntVar(&concurrency, "concurrency", 256, "Number of concurrent hosts to scan at once") 53 | RootCmd.PersistentFlags().StringVar(&dbPath, "db", "ssh_db.sqlite", "Path to database file") 54 | RootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "debug") 55 | } 56 | -------------------------------------------------------------------------------- /cmd/scan.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | log "github.com/inconshreveable/log15" 7 | "github.com/ncsa/ssh-auditor/sshauditor" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var scanCmd = &cobra.Command{ 12 | Use: "scan", 13 | Short: "Scan hosts using new or outdated credentials", 14 | Run: func(cmd *cobra.Command, args []string) { 15 | scanConfig := sshauditor.ScanConfiguration{ 16 | Concurrency: concurrency, 17 | } 18 | auditor := sshauditor.New(store) 19 | _, err := auditor.Scan(scanConfig) 20 | if err != nil { 21 | log.Error(err.Error()) 22 | os.Exit(1) 23 | } 24 | }, 25 | } 26 | 27 | var scanResetIntervalCmd = &cobra.Command{ 28 | Use: "reset", 29 | Aliases: []string{"r"}, 30 | Short: "reset interval", 31 | Run: func(cmd *cobra.Command, args []string) { 32 | err := store.ResetInterval() 33 | if err != nil { 34 | log.Error(err.Error()) 35 | os.Exit(1) 36 | } 37 | }, 38 | } 39 | 40 | func init() { 41 | RootCmd.AddCommand(scanCmd) 42 | scanCmd.AddCommand(scanResetIntervalCmd) 43 | } 44 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var ( 10 | version = "dev" 11 | ) 12 | 13 | var versionCmd = &cobra.Command{ 14 | Use: "version", 15 | Short: "Print the version number of ssh-auditor", 16 | // Don't create a store 17 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { return nil }, 18 | PersistentPostRunE: func(cmd *cobra.Command, args []string) error { return nil }, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | fmt.Println(version) 21 | }, 22 | } 23 | 24 | func init() { 25 | RootCmd.AddCommand(versionCmd) 26 | } 27 | -------------------------------------------------------------------------------- /cmd/vuln.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | log "github.com/inconshreveable/log15" 8 | "github.com/ncsa/ssh-auditor/sshauditor" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var vulnCmd = &cobra.Command{ 13 | Use: "vuln", 14 | Short: "Show vulnerabilities", 15 | Run: func(cmd *cobra.Command, args []string) { 16 | auditor := sshauditor.New(store) 17 | vulns, err := auditor.Vulnerabilities() 18 | if err != nil { 19 | log.Error(err.Error()) 20 | os.Exit(1) 21 | } 22 | for _, v := range vulns { 23 | fmt.Printf("%s\t%s\t%s\t%s\t%s\t%s\n", 24 | v.Host.Hostport, 25 | v.HostCredential.User, 26 | v.HostCredential.Password, 27 | v.HostCredential.Result, 28 | v.HostCredential.LastTested, 29 | v.Host.Version, 30 | ) 31 | } 32 | }, 33 | } 34 | 35 | func init() { 36 | RootCmd.AddCommand(vulnCmd) 37 | } 38 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | Render with https://github.com/justinazoff/spielbash 2 | 3 | spielbash --script demo/intro.yaml --output demo/intro.json 4 | -------------------------------------------------------------------------------- /demo/intro.json: -------------------------------------------------------------------------------- 1 | {"title": "ssh-auditor", "stdout": [[0.012964, "\u001b[?1049h\u001b[?1h\u001b=\u001b[H\u001b[2J\u001b[?12l\u001b[?25h\u001b[?1000l\u001b[?1002l\u001b[?1006l\u001b[?1005l\u001b[c\u001b(B\u001b[m\u001b[?12;25h\u001b[?12l\u001b[?25h\u001b[?1003l\u001b[?1006l\u001b[?2004l\u001b[1;1H\u001b[1;28r\u001b]112\u0007\u001b[1;3H\u001b[?25l\u001b[H$ \u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\r\n\u001b[K\u001b[1;3H\u001b[?12l\u001b[?25h"], [8.8e-05, "\u001b[H\u001b[K$ "], [0.658369, "#"], [0.07756, " "], [0.066779, "L"], [0.081035, "e"], [0.139582, "t"], [0.046255, "'"], [0.064172, "s"], [0.067656, " "], [0.114114, "g"], [0.059409, "e"], [0.031261, "t"], [0.125999, " "], [0.139505, "s"], [0.087246, "t"], [0.129401, "a"], [0.022307, "r"], [0.022679, "t"], [0.052775, "e"], [0.0767, "d"], [0.045966, "!"], [0.082827, "\r\n"], [9.6e-05, "$ "], [0.025929, "g"], [0.0542, "o"], [0.025235, " "], [0.043109, "g"], [0.048084, "e"], [0.0738, "t"], [0.119881, " "], [0.098995, "g"], [0.089028, "i"], [0.072922, "t"], [0.094713, "h"], [0.131132, "u"], [0.015947, "b"], [0.023176, "."], [0.080457, "c"], [0.061997, "o"], [0.015256, "m"], [0.076032, "/"], [0.028843, "n"], [0.092036, "c"], [0.132018, "s"], [0.063137, "a"], [0.126885, "/"], [0.119574, "s"], [0.070378, "s"], [0.060697, "h"], [0.083513, "-"], [0.064874, "a"], [0.052984, "u"], [0.094767, "d"], [0.054933, "i"], [0.027914, "t"], [0.069087, "o"], [0.047676, "r"], [0.067712, "\r\n"], [0.147643, "\u001b[?25l\u001b[?12l\u001b[?25h"], [0.859434, "$ \u001b[?25l\u001b[?12l\u001b[?25h"], [0.026888, "#"], [0.067119, " "], [0.103093, "E"], [0.023527, "n"], [0.10827, "s"], [0.064307, "u"], [0.101808, "r"], [0.052245, "e"], [0.027955, " "], [0.10414, "o"], [0.080031, "u"], [0.015243, "r"], [0.053186, " "], [0.062153, "p"], [0.106021, "a"], [0.020276, "t"], [0.089752, "h"], [0.050125, " "], [0.056419, "i"], [0.068205, "s"], [0.034225, " "], [0.128184, "s"], [0.0193, "e"], [0.029604, "t"], [0.068112, " "], [0.094828, "p"], [0.060835, "r"], [0.080153, "o"], [0.039274, "p"], [0.048135, "e"], [0.080276, "r"], [0.057089, "l"], [0.047867, "y"], [0.021912, "."], [0.072976, " "], [0.119964, "G"], [0.052533, "O"], [0.024859, "P"], [0.015468, "A"], [0.114335, "T"], [0.056597, "H"], [0.099734, " "], [0.103131, "i"], [0.114675, "s"], [0.048781, " "], [0.114545, "n"], [0.117313, "o"], [0.017295, "r"], [0.089371, "m"], [0.009272, "a"], [0.046971, "l"], [0.057078, "l"], [0.051598, "y"], [0.081361, " "], [0.0637, "~"], [0.101644, "/"], [0.078781, "g"], [0.079915, "o"], [0.124933, "/"], [0.047675, "\r\n"], [0.000164, "$ "], [0.026084, "P"], [0.136266, "A"], [0.0673, "T"], [0.105803, "H"], [0.100219, "="], [0.046797, "$"], [0.058567, "P"], [0.094807, "A"], [0.127572, "T"], [0.03399, "H"], [0.104378, ":"], [0.100318, "$"], [0.081633, "("], [0.020854, "g"], [0.08349, "o"], [0.059158, " "], [0.055252, "e"], [0.126044, "n"], [0.127934, "v"], [0.081136, " "], [0.065783, "G"], [0.135666, "O"], [0.028376, "P"], [0.07589, "A"], [0.137588, "T"], [0.055412, "H"], [0.089866, ")"], [0.051297, "\r\n"], [0.055691, "$ "], [0.174679, "s"], [0.060172, "s"], [0.046882, "h"], [0.024958, "-"], [0.044875, "a"], [0.051752, "u"], [0.088824, "d"], [0.11175, "i"], [0.121734, "t"], [0.045924, "o"], [0.124486, "r"], [2.136682, "\r\n"], [0.011043, "Complete documentation is available at https://github.com/ncsa/ssh-auditor\u001b[8;1H"], [0.000704, "Usage:\r\n ssh-auditor [command]\u001b[11;1HAvailable Commands:\r\n addcredential add a new credential pair\r\n discover discover new hosts\r\n dupes Show hosts using the same key\r\n logcheck trigger and report on failed ssh authentication attempts\r\n rescan Rescan hosts with credentials that have previously worked\r\n scan Scan hosts using new or outdated credentials\r\n vuln Show vulnerabilities\u001b[20;1HFlags:\r\n --db string Path to database file (default \"ssh_db.sqlite\")\r\n --debug debug\r\n -h, --help help for ssh-auditor\u001b[25;1HUse \"ssh-auditor [command] --help\" for more information about a command.\r\n"], [0.001767, "$ "], [0.043105, "#"], [0.066093, " "], [0.076144, "I"], [0.06216, "t"], [0.064876, " "], [0.021288, "l"], [0.114645, "o"], [0.02277, "o"], [0.071043, "k"], [0.033679, "s"], [0.022815, " "], [0.117273, "l"], [0.126534, "i"], [0.067056, "k"], [0.05025, "e"], [0.117614, " "], [0.058272, "i"], [0.030799, "t"], [0.059597, "'"], [0.033548, "s"], [0.048767, " "], [0.087718, "w"], [0.123294, "o"], [0.110367, "r"], [0.120081, "k"], [0.083385, "i"], [0.12207, "n"], [0.046152, "g"], [0.123125, "!"], [0.139688, "\r\n"], [4.4e-05, "$ "], [1.012963, "#"], [0.105791, " "], [0.057979, "F"], [0.136747, "i"], [0.107896, "r"], [0.034113, "s"], [0.075179, "t"], [0.059399, " "], [0.056212, "w"], [0.016504, "e"], [0.131824, " "], [0.105797, "n"], [0.089461, "e"], [0.11715, "e"], [0.10802, "d"], [0.009278, " "], [0.024641, "t"], [0.031152, "o"], [0.053588, " "], [0.087102, "d"], [0.117073, "i"], [0.04315, "s"], [0.097468, "c"], [0.104249, "o"], [0.123408, "v"], [0.040694, "e"], [0.119104, "r"], [0.137584, " "], [0.049682, "h"], [0.099502, "o"], [0.11208, "s"], [0.065817, "t"], [0.132844, "s"], [0.106216, " "], [0.085424, "r"], [0.048189, "u"], [0.092487, "n"], [0.130261, "n"], [0.12217, "i"], [0.070005, "n"], [0.047233, "g"], [0.032243, " "], [0.059044, "s"], [0.109415, "s"], [0.046428, "h"], [0.110353, "d"], [0.131512, "\r\n"], [0.000122, "$ "], [0.027511, "s"], [0.099577, "s"], [0.058518, "h"], [0.133136, "-"], [0.078672, "a"], [0.135341, "u"], [0.030857, "d"], [0.11858, "i"], [0.062762, "t"], [0.070259, "o"], [0.039474, "r"], [0.039937, " "], [0.06652, "d"], [0.04988, "i"], [0.134775, "s"], [0.131554, "c"], [0.127752, "o"], [0.128213, "v"], [0.06033, "e"], [0.135118, "r"], [0.112515, " "], [0.095447, "1"], [0.086244, "9"], [0.021423, "2"], [0.089393, "."], [0.043067, "1"], [0.068396, "6"], [0.097209, "8"], [0.056262, "."], [0.043652, "2"], [0.026925, "."], [0.086653, "0"], [0.099128, "/"], [0.032818, "2"], [0.011374, "4"], [2.080281, "\u001b[117C\n\r"], [0.017465, "\u001b[154C\n\u001b[27;1H\u001b[32mINFO\u001b[39m[08-04|15:56:43] discovering hosts \u001b[32minclude\u001b[39m=192.168.2.0/24 \u001b[32mexclude\u001b[39m= \u001b[32mtotal\u001b[39m=256 \u001b[32mports\u001b[39m=22\r\n"], [0.015219, "\u001b[154C\n\u001b[27;1H\u001b[32mINFO\u001b[39m[08-04|15:56:43] current known hosts \u001b[32mcount\u001b[39m=0\r\n"], [0.176315, "\u001b[2S\u001b[2A\u001b[32mINFO\u001b[39m[08-04|15:56:43] discovered new host \u001b[32mhost\u001b[39m=192.168.2.230:22 \u001b[32mversion\u001b[39m=SSH-2.0-OpenSSH_5.3 \u001b[32mfp\u001b[39m=SHA256:ZR/6ZXoEyhk5/T4jwgiKAchq8UOxVX7GTW0l7b92M7E\r\n"], [0.009665, "\u001b[2S\u001b[2A\u001b[32mINFO\u001b[39m[08-04|15:56:43] discovered new host \u001b[32mhost\u001b[39m=192.168.2.11:22 \u001b[32mversion\u001b[39m=SSH-2.0-OpenSSH_7.2 \u001b[32mfp\u001b[39m=SHA256:uH2v8JpIb1Hi0BwSm1VDy2R2STgUxjAG9tU5GQlAzdA\r\n"], [0.03013, "\u001b[2S\u001b[2A\u001b[32mINFO\u001b[39m[08-04|15:56:43] discovered new host \u001b[32mhost\u001b[39m=192.168.2.23:22 \u001b[32mversion\u001b[39m=\"SSH-2.0-OpenSSH_6.7p1 Debian-5+deb8u2\" \u001b[32mfp\u001b[39m=SHA256:WJA9de4bTHnZ2a5+UbtKrxsyL8M/IBEObsf3ubyXR5U\r\n"], [0.032343, "\u001b[2S\u001b[2A\u001b[32mINFO\u001b[39m[08-04|15:56:43] discovered new host \u001b[32mhost\u001b[39m=192.168.2.224:22 \u001b[32mversion\u001b[39m=SSH-2.0-OpenSSH_6.6.1 \u001b[32mfp\u001b[39m=SHA256:p907Cr9DI1sCV7TP7RoiQxVwYHvTt0g6uLkfiB7Qgjc\r\n"], [0.024882, "\u001b[4S\u001b[4A\u001b[32mINFO\u001b[39m[08-04|15:56:43] discovered new host \u001b[32mhost\u001b[39m=192.168.2.173:22 \u001b[32mversion\u001b[39m=\"SSH-2.0-OpenSSH_6.6.1p1 Ubuntu-2ubuntu2.8\" \u001b[32mfp\u001b[39m=SHA256:BYYPcC8lSBq0j7pxjhcI1NLtv1XVPYdb/Uo1m6IpXVk\r\n\u001b[32mINFO\u001b[39m[08-04|15:56:43] discovered new host \u001b[32mhost\u001b[39m=192.168.2.20:22 \u001b[32mversion\u001b[39m=\"SSH-2.0-OpenSSH_6.7p1 Debian-5+deb8u3\" \u001b[32mfp\u001b[39m=SHA256:fWE6nW2j8VpSbftWfThX9ZvEXFZnkU+ubGPcdWSfg1s\r\n"], [0.015831, "\u001b[2S\u001b[2A\u001b[32mINFO\u001b[39m[08-04|15:56:43] discovered new host \u001b[32mhost\u001b[39m=192.168.2.25:22 \u001b[32mversion\u001b[39m=\"SSH-2.0-OpenSSH_6.7p1 Debian-5+deb8u3\" \u001b[32mfp\u001b[39m=SHA256:B7x722JNCJTq0hq53+g8ZstSaVY+2K3LeiPcf5LZOfc\r\n"], [0.209303, "\u001b[2S\u001b[2A\u001b[32mINFO\u001b[39m[08-04|15:56:44] discovered new host \u001b[32mhost\u001b[39m=192.168.2.166:22 \u001b[32mversion\u001b[39m=SSH-2.0-dropbear_2016.74 \u001b[32mfp\u001b[39m=SHA256:Bi2yH5lu+H2qbND3OQNSiTFvP0DBKPAGBZAxH5WXxCc\u001b[?25l\r\n\u001b[?12l\u001b[?25h"], [1.29496, "\u001b[2S\u001b[2A\u001b[32mINFO\u001b[39m[08-04|15:56:45] discovered new host \u001b[32mhost\u001b[39m=192.168.2.21:22 \u001b[32mversion\u001b[39m=\"SSH-2.0-OpenSSH_6.7p1 Debian-5+deb8u2\" \u001b[32mfp\u001b[39m=SHA256:xXLBit/lM6IMS3h5uPCPZVEeh86nweQRrF3SJn5Cslg\r\n"], [0.011288, "\u001b[2S\u001b[2A\u001b[32mINFO\u001b[39m[08-04|15:56:45] discovered new host \u001b[32mhost\u001b[39m=192.168.2.238:22 \u001b[32mversion\u001b[39m=\"SSH-2.0-OpenSSH_6.7p1 Debian-5+deb8u2\" \u001b[32mfp\u001b[39m=SHA256:Qe5qlEZWlcK5ww2Vgn5RCz5Sd5sTGGcDtE+opSmpiwk\r\n"], [0.00955, "\u001b[2S\u001b[2A\u001b[32mINFO\u001b[39m[08-04|15:56:45] discovered new host \u001b[32mhost\u001b[39m=192.168.2.157:22 \u001b[32mversion\u001b[39m=\"SSH-2.0-OpenSSH_6.7p1 Ubuntu-5ubuntu1.3\" \u001b[32mfp\u001b[39m=SHA256:bjh95SBXyQ6Pr6vFkLhHQ9fRmVWZ4d3MM7JXWLmMLcY\r\n"], [0.018377, "\u001b[2S\u001b[2A\u001b[32mINFO\u001b[39m[08-04|15:56:45] discovered new host \u001b[32mhost\u001b[39m=192.168.2.179:22 \u001b[32mversion\u001b[39m=\"SSH-2.0-OpenSSH_6.7p1 Debian-5+deb8u2\" \u001b[32mfp\u001b[39m=SHA256:TRe2T/eKPAuEJHceHT1BlSjsb7szIUFukKVW9uvweMw\r\n"], [0.022123, "\u001b[2S\u001b[2A\u001b[32mINFO\u001b[39m[08-04|15:56:45] discovered new host \u001b[32mhost\u001b[39m=192.168.2.22:22 \u001b[32mversion\u001b[39m=\"SSH-2.0-OpenSSH_6.7p1 Debian-5+deb8u2\" \u001b[32mfp\u001b[39m=SHA256:UncQx7cBkvivaw9/829UMjg39vanlsC1cUkqZXDCGSQ\r\n"], [0.017524, "\u001b[2S\u001b[2A\u001b[32mINFO\u001b[39m[08-04|15:56:45] discovered new host \u001b[32mhost\u001b[39m=192.168.2.183:22 \u001b[32mversion\u001b[39m=SSH-2.0-OpenSSH_7.4 \u001b[32mfp\u001b[39m=SHA256:FGBYqI7ycE9q0+4dK3pbQZArxAboE9kIuMSmY/iG3zg\r\n"], [0.016013, "\u001b[2S\u001b[2A\u001b[32mINFO\u001b[39m[08-04|15:56:45] discovered new host \u001b[32mhost\u001b[39m=192.168.2.182:22 \u001b[32mversion\u001b[39m=SSH-2.0-OpenSSH_7.4 \u001b[32mfp\u001b[39m=SHA256:JhanElfccLus7NW+sQjGcCNJxeF+cO/a1ut3LIo7qYU\r\n"], [0.022976, "\u001b[2S\u001b[2A\u001b[32mINFO\u001b[39m[08-04|15:56:45] discovered new host \u001b[32mhost\u001b[39m=192.168.2.181:22 \u001b[32mversion\u001b[39m=SSH-2.0-OpenSSH_7.4 \u001b[32mfp\u001b[39m=SHA256:frSvtg4rUo27brRkwsN9oCJpW7Pb6AWrdXI902UPMaw\r\n"], [0.919509, "\u001b[2S\u001b[2A\u001b[32mINFO\u001b[39m[08-04|15:56:46] discovered new host \u001b[32mhost\u001b[39m=192.168.2.1:22 \u001b[32mversion\u001b[39m=\"SSH-2.0-OpenSSH_6.0p1 Debian-4+deb7u2\" \u001b[32mfp\u001b[39m=SHA256:YJbW5axwm1wfkcF07i7qK4YB3M+KXwSaoThYtoi6DR0\r\n"], [0.92452, "\u001b[2S\u001b[2A\u001b[32mINFO\u001b[39m[08-04|15:56:47] discovered new host \u001b[32mhost\u001b[39m=192.168.2.243:22 \u001b[32mversion\u001b[39m=SSH-1.99-OpenSSH_4.3 \u001b[32mfp\u001b[39m=SHA256:CGrCFihPwBWY3BjkEHHNcovEID5r1vPLNC3A8p0AyPc\r\n"], [4.283982, "\u001b[3S\u001b[3A\u001b[32mINFO\u001b[39m[08-04|15:56:51] discovered new host \u001b[32mhost\u001b[39m=192.168.2.10:22 \u001b[32mversion\u001b[39m=SSH-2.0-mpSSH_0.2.1 \u001b[32mfp\u001b[39m=SHA256:eyoWnkiKFH50m85T5fHjauk+wRhB6fCpmpSeSsSIBaI\r\n\u001b[32mINFO\u001b[39m[08-04|15:56:51] discovery report \u001b[32mtotal\u001b[39m=19 \u001b[32mnew\u001b[39m=19 \u001b[32mupdated\u001b[39m=0\r\n"], [0.000426, "\u001b[154C\n\u001b[27;1H\u001b[32mINFO\u001b[39m[08-04|15:56:51] brute force queue size \u001b[32mnew\u001b[39m=0 \u001b[32mtotal\u001b[39m=0\r\n"], [0.005609, "$ "], [0.093842, "#"], [0.100472, " "], [0.118338, "N"], [0.096952, "o"], [0.032913, "w"], [0.100246, "\u001b[?25l\u001b[?12l\u001b[?25h"], [0.002863, " "], [0.130821, "t"], [0.126854, "h"], [0.105241, "a"], [0.097493, "t"], [0.055012, " "], [0.072352, "w"], [0.051582, "e"], [0.07599, " "], [0.021942, "h"], [0.109687, "a"], [0.042767, "v"], [0.048913, "e"], [0.074579, " "], [0.051935, "s"], [0.072138, "o"], [0.127301, "m"], [0.024342, "e"], [0.122939, " "], [0.132309, "h"], [0.053501, "o"], [0.057496, "s"], [0.130705, "t"], [0.114103, "s"], [0.109247, ","], [0.114607, " "], [0.084347, "c"], [0.018001, "a"], [0.073753, "n"], [0.034178, " "], [0.013501, "w"], [0.050869, "e"], [0.106837, " "], [0.021874, "s"], [0.082461, "c"], [0.064511, "a"], [0.123881, "n"], [0.124916, " "], [0.123198, "t"], [0.055728, "h"], [0.075358, "e"], [0.046404, "n"], [0.096303, "?"], [0.124674, "\u001b[104C\n\r"], [0.000104, "$ "], [0.025849, "s"], [0.125422, "s"], [0.100577, "h"], [0.119412, "-"], [0.052506, "a"], [0.106581, "u"], [0.009569, "d"], [0.032255, "i"], [0.087917, "t"], [0.083841, "o"], [0.086247, "r"], [0.020259, " "], [0.104341, "s"], [0.096564, "c"], [0.096893, "a"], [0.117778, "n"], [0.099417, " "], [0.021467, "-"], [0.101593, "-"], [0.083673, "d"], [0.08821, "e"], [0.07463, "b"], [0.100152, "u"], [0.036766, "g"], [0.058176, "\u001b[128C\n\r"], [0.015436, "\u001b[154C\n\u001b[27;1H\u001b[32mINFO\u001b[39m[08-04|15:56:57] brute force queue size \u001b[32mnew\u001b[39m=0 \u001b[32mtotal\u001b[39m=0\r\n"], [0.000937, "\u001b[154C\n\u001b[27;1H\u001b[32mINFO\u001b[39m[08-04|15:56:57] brute force scan report \u001b[32mtotal\u001b[39m=0 \u001b[32mneg\u001b[39m=0 \u001b[32mpos\u001b[39m=0 \u001b[32merr\u001b[39m=0\r\n"], [0.002202, "$ "], [0.04377, "#"], [0.040118, " "], [0.105852, "N"], [0.085394, "o"], [0.072408, "t"], [0.100761, " "], [0.139714, "y"], [0.068217, "e"], [0.057646, "t"], [0.109913, "!"], [0.074515, " "], [0.02483, " "], [0.127537, "B"], [0.082643, "e"], [0.057892, "f"], [0.082588, "o"], [0.016762, "r"], [0.049674, "e"], [0.051908, " "], [0.048739, "w"], [0.049635, "e"], [0.080794, " "], [0.136246, "c"], [0.057091, "a"], [0.139515, "n"], [0.115635, " "], [0.10458, "s"], [0.131454, "c"], [0.084351, "a"], [0.037342, "n"], [0.069848, " "], [0.045416, "t"], [0.070826, "h"], [0.05391, "e"], [0.009918, " "], [0.045883, "h"], [0.053613, "o"], [0.112388, "s"], [0.030134, "t"], [0.087478, "s"], [0.099264, ","], [0.025374, " "], [0.124785, "w"], [0.090265, "e"], [0.087624, " "], [0.125243, "n"], [0.098847, "e"], [0.079886, "e"], [0.067141, "d"], [0.076372, " "], [0.120053, "c"], [0.06675, "r"], [0.106367, "e"], [0.040647, "d"], [0.08839, "e"], [0.085365, "n"], [0.080213, "t"], [0.126743, "i"], [0.129072, "a"], [0.0665, "l"], [0.057688, "s"], [0.074769, "\u001b[91C\n\r"], [4.1e-05, "$ "], [0.008019, "#"], [0.127342, " "], [0.015026, "L"], [0.050355, "e"], [0.055003, "t"], [0.040592, "'"], [0.112482, "s"], [0.091951, " "], [0.08376, "a"], [0.100535, "d"], [0.030114, "d"], [0.092805, " "], [0.084571, "s"], [0.021213, "o"], [0.069367, "m"], [0.047745, "e"], [0.121013, "!"], [0.01511, "\u001b[135C\n\r"], [9.5e-05, "$ "], [1.03258, "s"], [0.136338, "s"], [0.100365, "h"], [0.106304, "-"], [0.042576, "a"], [0.132149, "u"], [0.1096, "d"], [0.084106, "i"], [0.065955, "t"], [0.059313, "o"], [0.10801, "r"], [0.0586, " "], [0.086677, "a"], [0.099524, "d"], [0.080725, "d"], [0.06813, "c"], [0.121915, "r"], [0.089012, "e"], [0.100856, "d"], [0.092869, "e"], [0.071531, "n"], [0.061877, "t"], [0.061322, "i"], [0.096352, "a"], [0.069289, "l"], [0.134247, " "], [0.088134, "r"], [0.089749, "o"], [0.074112, "o"], [0.127035, "t"], [0.047266, " "], [0.104567, "r"], [0.081409, "o"], [0.018992, "o"], [0.081515, "t"], [1.03541, "\u001b[117C\n\r"], [0.017185, "\u001b[154C\n\u001b[27;1H\u001b[32mINFO\u001b[39m[08-04|15:57:08] added credential \u001b[32muser\u001b[39m=root \u001b[32mpassword\u001b[39m=root \u001b[32minterval\u001b[39m=14\r\n"], [0.003181, "$ "], [0.044145, "#"], [0.104788, " "], [0.066429, "a"], [0.046476, "c"], [0.072288, " "], [0.118685, "i"], [0.126486, "s"], [0.017397, " "], [0.083565, "a"], [0.046386, "n"], [0.104673, " "], [0.018125, "a"], [0.054209, "l"], [0.05083, "i"], [0.134316, "a"], [0.136938, "s"], [0.131223, " "], [0.075027, "f"], [0.118782, "o"], [0.065741, "r"], [0.042367, " "], [0.073952, "a"], [0.017557, "d"], [0.07093, "d"], [0.053147, "c"], [0.058795, "r"], [0.028101, "e"], [0.042581, "d"], [0.077552, "e"], [0.099245, "n"], [0.062013, "t"], [0.0236, "i"], [0.057893, "a"], [0.131288, "l"], [0.11311, "\u001b[118C\n\r"], [0.000206, "$ "], [0.026011, "s"], [0.021322, "s"], [0.13463, "h"], [0.124598, "-"], [0.098578, "a"], [0.079613, "u"], [0.130117, "d"], [0.058009, "i"], [0.015302, "t"], [0.044999, "o"], [0.085938, "r"], [0.108647, " "], [0.054662, "a"], [0.107873, "c"], [0.02132, " "], [0.080973, "a"], [0.115455, "d"], [0.022735, "m"], [0.126636, "i"], [0.015147, "n"], [0.118925, " "], [0.079609, "a"], [0.110688, "d"], [0.098271, "m"], [0.015241, "i"], [0.025993, "n"], [0.114657, "\u001b[126C\n\r"], [0.017052, "\u001b[154C\n\u001b[27;1H\u001b[32mINFO\u001b[39m[08-04|15:57:13] added credential \u001b[32muser\u001b[39m=admin \u001b[32mpassword\u001b[39m=admin \u001b[32minterval\u001b[39m=14\r\n"], [0.002466, "$ "], [0.039022, "#"], [0.114357, " "], [0.088101, "N"], [0.073659, "o"], [0.11268, "w"], [0.043877, " "], [0.05522, "t"], [0.051593, "h"], [0.022441, "a"], [0.084644, "t"], [0.129771, " "], [0.031863, "w"], [0.032576, "e"], [0.124366, " "], [0.107528, "h"], [0.051921, "a"], [0.12103, "v"], [0.095706, "e"], [0.080661, " "], [0.024266, "c"], [0.034754, "r"], [0.013435, "e"], [0.033396, "d"], [0.121121, "e"], [0.064066, "n"], [0.058654, "t"], [0.059585, "i"], [0.045319, "a"], [0.052202, "l"], [0.079629, "s"], [0.084241, ","], [0.117298, " "], [0.122603, "c"], [0.089696, "a"], [0.047175, "n"], [0.091157, " "], [0.105549, "w"], [0.114718, "e"], [0.016018, " "], [0.091833, "s"], [0.024084, "c"], [0.033668, "a"], [0.09983, "n"], [0.089425, " "], [0.078295, "t"], [0.049737, "h"], [0.031204, "e"], [0.043403, " "], [0.082599, "h"], [0.032955, "o"], [0.10505, "s"], [0.076495, "t"], [0.041402, "s"], [0.018623, " "], [0.071015, "n"], [0.105285, "o"], [0.020581, "w"], [0.064615, "?"], [0.011448, "\u001b[94C\n\r"], [8.9e-05, "$ "], [0.030758, "s"], [0.102599, "s"], [0.138435, "h"], [0.027521, "-"], [0.088624, "a"], [0.015429, "u"], [0.053386, "d"], [0.054409, "i"], [0.039167, "t"], [0.029403, "o"], [0.119918, "r"], [0.0603, " "], [0.084696, "s"], [0.126075, "c"], [0.045995, "a"], [0.126226, "n"], [0.073753, " "], [0.044861, "-"], [0.014575, "-"], [0.038087, "d"], [0.043337, "e"], [0.114657, "b"], [0.112386, "u"], [0.030582, "g"], [2.045962, "\u001b[128C\n\r"], [0.016259, "\u001b[154C\n\u001b[27;1H\u001b[32mINFO\u001b[39m[08-04|15:57:20] brute force queue size \u001b[32mnew\u001b[39m=38 \u001b[32mtotal\u001b[39m=38\r\n"], [0.06961, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:21] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.230:22 \u001b[36muser\u001b[39m=root \u001b[36mpassword\u001b[39m=root \u001b[36mresult\u001b[39m=\r\n"], [0.060224, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:21] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.230:22 \u001b[36muser\u001b[39m=admin \u001b[36mpassword\u001b[39m=admin \u001b[36mresult\u001b[39m=\r\n"], [0.045518, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:21] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.173:22 \u001b[36muser\u001b[39m=root \u001b[36mpassword\u001b[39m=root \u001b[36mresult\u001b[39m=\r\n"], [0.028491, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:21] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.25:22 \u001b[36muser\u001b[39m=root \u001b[36mpassword\u001b[39m=root \u001b[36mresult\u001b[39m=\r\n"], [0.02514, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:21] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.23:22 \u001b[36muser\u001b[39m=root \u001b[36mpassword\u001b[39m=root \u001b[36mresult\u001b[39m=\r\n"], [0.011889, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:21] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.173:22 \u001b[36muser\u001b[39m=admin \u001b[36mpassword\u001b[39m=admin \u001b[36mresult\u001b[39m=\r\n"], [0.006481, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:21] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.224:22 \u001b[36muser\u001b[39m=root \u001b[36mpassword\u001b[39m=root \u001b[36mresult\u001b[39m=\r\n"], [0.012047, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:21] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.25:22 \u001b[36muser\u001b[39m=admin \u001b[36mpassword\u001b[39m=admin \u001b[36mresult\u001b[39m=\r\n"], [0.018013, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:21] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.11:22 \u001b[36muser\u001b[39m=root \u001b[36mpassword\u001b[39m=root \u001b[36mresult\u001b[39m=\r\n"], [0.028429, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:21] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.23:22 \u001b[36muser\u001b[39m=admin \u001b[36mpassword\u001b[39m=admin \u001b[36mresult\u001b[39m=\r\n"], [0.014672, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:21] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.20:22 \u001b[36muser\u001b[39m=root \u001b[36mpassword\u001b[39m=root \u001b[36mresult\u001b[39m=\r\n"], [0.002926, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:21] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.224:22 \u001b[36muser\u001b[39m=admin \u001b[36mpassword\u001b[39m=admin \u001b[36mresult\u001b[39m=\r\n"], [0.016011, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:21] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.11:22 \u001b[36muser\u001b[39m=admin \u001b[36mpassword\u001b[39m=admin \u001b[36mresult\u001b[39m=\r\n"], [0.033222, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:21] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.20:22 \u001b[36muser\u001b[39m=admin \u001b[36mpassword\u001b[39m=admin \u001b[36mresult\u001b[39m=\r\n"], [0.041023, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:21] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.166:22 \u001b[36muser\u001b[39m=root \u001b[36mpassword\u001b[39m=root \u001b[36mresult\u001b[39m=\r\n"], [0.146157, "\u001b[?25l\u001b[?12l\u001b[?25h"], [0.267595, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:21] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.166:22 \u001b[36muser\u001b[39m=admin \u001b[36mpassword\u001b[39m=admin \u001b[36mresult\u001b[39m=\r\n"], [0.338534, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:22] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.243:22 \u001b[36muser\u001b[39m=root \u001b[36mpassword\u001b[39m=root \u001b[36mresult\u001b[39m=\r\n"], [1.079376, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:23] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.22:22 \u001b[36muser\u001b[39m=root \u001b[36mpassword\u001b[39m=root \u001b[36mresult\u001b[39m=\r\n"], [0.005117, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:23] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.21:22 \u001b[36muser\u001b[39m=root \u001b[36mpassword\u001b[39m=root \u001b[36mresult\u001b[39m=\r\n"], [0.034238, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:23] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.179:22 \u001b[36muser\u001b[39m=root \u001b[36mpassword\u001b[39m=root \u001b[36mresult\u001b[39m=\r\n"], [0.152327, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:23] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.238:22 \u001b[36muser\u001b[39m=root \u001b[36mpassword\u001b[39m=root \u001b[36mresult\u001b[39m=\r\n"], [0.088983, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:23] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.157:22 \u001b[36muser\u001b[39m=root \u001b[36mpassword\u001b[39m=root \u001b[36mresult\u001b[39m=\r\n"], [0.045864, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:23] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.183:22 \u001b[36muser\u001b[39m=root \u001b[36mpassword\u001b[39m=root \u001b[36mresult\u001b[39m=\r\n"], [0.066971, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:23] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.181:22 \u001b[36muser\u001b[39m=root \u001b[36mpassword\u001b[39m=root \u001b[36mresult\u001b[39m=\r\n"], [0.048665, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:23] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.182:22 \u001b[36muser\u001b[39m=root \u001b[36mpassword\u001b[39m=root \u001b[36mresult\u001b[39m=\r\n"], [0.043212, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:23] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.1:22 \u001b[36muser\u001b[39m=root \u001b[36mpassword\u001b[39m=root \u001b[36mresult\u001b[39m=\r\n"], [0.445287, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:24] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.243:22 \u001b[36muser\u001b[39m=admin \u001b[36mpassword\u001b[39m=admin \u001b[36mresult\u001b[39m=\r\n"], [1.486529, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:25] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.1:22 \u001b[36muser\u001b[39m=admin \u001b[36mpassword\u001b[39m=admin \u001b[36mresult\u001b[39m=\r\n"], [0.064169, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:25] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.21:22 \u001b[36muser\u001b[39m=admin \u001b[36mpassword\u001b[39m=admin \u001b[36mresult\u001b[39m=\r\n"], [0.00163, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:25] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.22:22 \u001b[36muser\u001b[39m=admin \u001b[36mpassword\u001b[39m=admin \u001b[36mresult\u001b[39m=\r\n"], [0.029445, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:25] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.179:22 \u001b[36muser\u001b[39m=admin \u001b[36mpassword\u001b[39m=admin \u001b[36mresult\u001b[39m=\r\n"], [0.177966, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:25] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.238:22 \u001b[36muser\u001b[39m=admin \u001b[36mpassword\u001b[39m=admin \u001b[36mresult\u001b[39m=\r\n"], [0.146992, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:26] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.157:22 \u001b[36muser\u001b[39m=admin \u001b[36mpassword\u001b[39m=admin \u001b[36mresult\u001b[39m=\r\n"], [0.013765, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:26] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.183:22 \u001b[36muser\u001b[39m=admin \u001b[36mpassword\u001b[39m=admin \u001b[36mresult\u001b[39m=\r\n"], [0.032553, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:26] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.181:22 \u001b[36muser\u001b[39m=admin \u001b[36mpassword\u001b[39m=admin \u001b[36mresult\u001b[39m=\r\n"], [0.066021, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:26] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.182:22 \u001b[36muser\u001b[39m=admin \u001b[36mpassword\u001b[39m=admin \u001b[36mresult\u001b[39m=\r\n"], [2.821619, "\u001b[2S\u001b[2A\u001b[31mEROR\u001b[39m[08-04|15:57:29] brute force error \u001b[31mhost\u001b[39m=192.168.2.10:22 \u001b[31muser\u001b[39m=root \u001b[31mpassword\u001b[39m=root \u001b[31mresult\u001b[39m= \u001b[31merr\u001b[39m=\"ssh: handshake failed: read tcp 192.168.2.144:57792->192.168.2.10:22: i/o timeout\"\r\n"], [8.020121, "\u001b[2S\u001b[2A\u001b[31mEROR\u001b[39m[08-04|15:57:37] brute force error \u001b[31mhost\u001b[39m=192.168.2.10:22 \u001b[31muser\u001b[39m=admin \u001b[31mpassword\u001b[39m=admin \u001b[31mresult\u001b[39m= \u001b[31merr\u001b[39m=\"ssh: handshake failed: read tcp 192.168.2.144:57844->192.168.2.10:22: i/o timeout\"\r\n"], [9.7e-05, "\u001b[154C\n\u001b[27;1H\u001b[32mINFO\u001b[39m[08-04|15:57:37] brute force scan report \u001b[32mtotal\u001b[39m=38 \u001b[32mneg\u001b[39m=36 \u001b[32mpos\u001b[39m=0 \u001b[32merr\u001b[39m=2\r\n"], [0.004221, "$ "], [0.158716, "s"], [0.06819, "s"], [0.123622, "h"], [0.094663, "-"], [0.127841, "\u001b[?25l\u001b[?12l\u001b[?25h"], [0.002879, "a"], [0.029924, "u"], [0.032805, "d"], [0.097098, "i"], [0.119402, "t"], [0.119329, "o"], [0.070578, "r"], [0.018283, " "], [0.071317, "v"], [0.132647, "u"], [0.112723, "l"], [0.110536, "n"], [2.104911, "\u001b[136C\n\r"], [0.02113, "$ "], [0.040955, "#"], [0.132742, " "], [0.124559, "N"], [0.101657, "o"], [0.039819, "t"], [0.050489, "h"], [0.1347, "i"], [0.069054, "n"], [0.034336, "g"], [0.069608, " "], [0.023956, "v"], [0.054489, "u"], [0.111852, "l"], [0.104712, "n"], [0.099361, "e"], [0.021063, "r"], [0.042033, "a"], [0.076557, "b"], [0.066881, "l"], [0.078622, "e"], [0.026562, ","], [0.013219, " "], [0.074821, "s"], [0.024308, "o"], [0.076381, " "], [0.105873, "f"], [0.039103, "a"], [0.052464, "r"], [0.037295, " "], [0.045614, "s"], [0.124223, "o"], [0.07574, " "], [0.102052, "g"], [0.05394, "o"], [0.109239, "o"], [0.046548, "d"], [0.133914, "\u001b[116C\n\r"], [0.000117, "$ "], [0.008532, "#"], [0.065111, " "], [0.074568, "I"], [0.089603, "f"], [0.051719, " "], [0.10256, "w"], [0.036778, "e"], [0.124453, " "], [0.017908, "a"], [0.008983, "d"], [0.046962, "d"], [0.11996, " "], [0.109829, "a"], [0.023665, " "], [0.135319, "n"], [0.133738, "e"], [0.087588, "w"], [0.127106, " "], [0.060377, "c"], [0.114457, "r"], [0.058181, "e"], [0.104639, "d"], [0.043573, "e"], [0.079751, "n"], [0.048048, "t"], [0.108552, "i"], [0.050567, "a"], [0.13013, "l"], [0.117441, ","], [0.126213, " "], [0.021465, "s"], [0.036675, "s"], [0.109604, "h"], [0.137389, "-"], [0.085738, "a"], [0.122968, "u"], [0.116463, "d"], [0.077003, "i"], [0.135356, "t"], [0.024761, "o"], [0.022182, "r"], [0.0623, " "], [0.139018, "w"], [0.034036, "i"], [0.026157, "l"], [0.091446, "l"], [0.083462, " "], [0.106889, "s"], [0.059651, "c"], [0.134426, "a"], [0.021642, "n"], [0.051451, " "], [0.116325, "a"], [0.063302, "l"], [0.076424, "l"], [0.011648, " "], [0.08098, "k"], [0.039735, "n"], [0.092574, "o"], [0.105333, "w"], [0.061446, "n"], [0.010866, " "], [0.11388, "h"], [0.137771, "o"], [0.125133, "s"], [0.075059, "t"], [0.0393, "s"], [0.110004, " "], [0.081934, "w"], [0.027532, "i"], [0.040883, "t"], [0.017098, "h"], [0.066027, " "], [0.099192, "t"], [0.110625, "h"], [0.048494, "a"], [0.127132, "t"], [0.061623, " "], [0.111585, "n"], [0.080349, "e"], [0.026442, "w"], [0.080158, " "], [0.082296, "c"], [0.106452, "r"], [0.057509, "e"], [0.084851, "d"], [0.047932, "e"], [0.071888, "n"], [0.0826, "t"], [0.019252, "i"], [0.103484, "a"], [0.049982, "l"], [0.104334, "\u001b[60C\n\r"], [8.7e-05, "$ "], [0.02565, "s"], [0.134917, "s"], [0.050317, "h"], [0.059739, "-"], [0.065212, "a"], [0.131417, "u"], [0.128236, "d"], [0.085119, "i"], [0.093526, "t"], [0.05654, "o"], [0.071634, "r"], [0.008924, " "], [0.044295, "a"], [0.073261, "c"], [0.1223, " "], [0.046108, "t"], [0.091096, "e"], [0.048314, "s"], [0.119024, "t"], [0.12847, " "], [0.059809, "t"], [0.027352, "e"], [0.110453, "s"], [0.025429, "t"], [0.090197, "\u001b[128C\n\r"], [0.013022, "\u001b[154C\n\u001b[27;1H\u001b[32mINFO\u001b[39m[08-04|15:57:52] added credential \u001b[32muser\u001b[39m=test \u001b[32mpassword\u001b[39m=test \u001b[32minterval\u001b[39m=14\r\n"], [0.002344, "$ "], [0.066075, "s"], [0.083907, "s"], [0.126826, "h"], [0.046833, "-"], [0.089905, "a"], [0.0866, "u"], [0.018134, "d"], [0.008576, "i"], [0.069633, "t"], [0.070097, "o"], [0.134198, "r"], [0.074702, " "], [0.050895, "s"], [0.042796, "c"], [0.031196, "a"], [0.110122, "n"], [0.126708, " "], [0.058697, "-"], [0.134703, "-"], [0.070275, "d"], [0.030228, "e"], [0.076743, "b"], [0.11783, "u"], [0.125038, "g"], [0.019088, "\u001b[128C\n\r"], [0.022699, "\u001b[154C\n\u001b[27;1H\u001b[32mINFO\u001b[39m[08-04|15:57:54] brute force queue size \u001b[32mnew\u001b[39m=19 \u001b[32mtotal\u001b[39m=19\r\n"], [0.042476, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:54] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.230:22 \u001b[36muser\u001b[39m=test \u001b[36mpassword\u001b[39m=test \u001b[36mresult\u001b[39m=\r\n"], [0.076207, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:54] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.173:22 \u001b[36muser\u001b[39m=test \u001b[36mpassword\u001b[39m=test \u001b[36mresult\u001b[39m=\r\n"], [0.091862, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:54] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.23:22 \u001b[36muser\u001b[39m=test \u001b[36mpassword\u001b[39m=test \u001b[36mresult\u001b[39m=\r\n"], [0.050054, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:54] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.224:22 \u001b[36muser\u001b[39m=test \u001b[36mpassword\u001b[39m=test \u001b[36mresult\u001b[39m=\r\n"], [0.040088, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:54] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.11:22 \u001b[36muser\u001b[39m=test \u001b[36mpassword\u001b[39m=test \u001b[36mresult\u001b[39m=\r\n"], [0.032233, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:54] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.20:22 \u001b[36muser\u001b[39m=test \u001b[36mpassword\u001b[39m=test \u001b[36mresult\u001b[39m=\r\n"], [0.045867, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:54] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.25:22 \u001b[36muser\u001b[39m=test \u001b[36mpassword\u001b[39m=test \u001b[36mresult\u001b[39m=\r\n"], [0.06266, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:54] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.166:22 \u001b[36muser\u001b[39m=test \u001b[36mpassword\u001b[39m=test \u001b[36mresult\u001b[39m=\u001b[?25l\r\n\u001b[?12l\u001b[?25h"], [1.011156, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:55] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.243:22 \u001b[36muser\u001b[39m=test \u001b[36mpassword\u001b[39m=test \u001b[36mresult\u001b[39m=\r\n"], [0.660045, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:56] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.21:22 \u001b[36muser\u001b[39m=test \u001b[36mpassword\u001b[39m=test \u001b[36mresult\u001b[39m=\r\n"], [0.013856, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:56] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.157:22 \u001b[36muser\u001b[39m=test \u001b[36mpassword\u001b[39m=test \u001b[36mresult\u001b[39m=\r\n"], [0.074128, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:56] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.22:22 \u001b[36muser\u001b[39m=test \u001b[36mpassword\u001b[39m=test \u001b[36mresult\u001b[39m=\r\n"], [0.040419, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:56] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.183:22 \u001b[36muser\u001b[39m=test \u001b[36mpassword\u001b[39m=test \u001b[36mresult\u001b[39m=\r\n"], [0.017569, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:56] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.238:22 \u001b[36muser\u001b[39m=test \u001b[36mpassword\u001b[39m=test \u001b[36mresult\u001b[39m=\r\n\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:56] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.179:22 \u001b[36muser\u001b[39m=test \u001b[36mpassword\u001b[39m=test \u001b[36mresult\u001b[39m=\r\n"], [0.008107, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:56] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.182:22 \u001b[36muser\u001b[39m=test \u001b[36mpassword\u001b[39m=test \u001b[36mresult\u001b[39m=\r\n"], [0.101492, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:56] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.1:22 \u001b[36muser\u001b[39m=test \u001b[36mpassword\u001b[39m=test \u001b[36mresult\u001b[39m=\r\n"], [0.009467, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:57:56] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.181:22 \u001b[36muser\u001b[39m=test \u001b[36mpassword\u001b[39m=test \u001b[36mresult\u001b[39m=\r\n"], [5.639828, "\u001b[2S\u001b[2A\u001b[31mEROR\u001b[39m[08-04|15:58:02] brute force error \u001b[31mhost\u001b[39m=192.168.2.10:22 \u001b[31muser\u001b[39m=test \u001b[31mpassword\u001b[39m=test \u001b[31mresult\u001b[39m= \u001b[31merr\u001b[39m=\"ssh: handshake failed: read tcp 192.168.2.144:57964->192.168.2.10:22: i/o timeout\"\r\n"], [5.4e-05, "\u001b[154C\n\u001b[27;1H\u001b[32mINFO\u001b[39m[08-04|15:58:02] brute force scan report \u001b[32mtotal\u001b[39m=19 \u001b[32mneg\u001b[39m=18 \u001b[32mpos\u001b[39m=0 \u001b[32merr\u001b[39m=1\r\n"], [0.003949, "$ "], [0.090491, "#"], [0.064944, " "], [0.102338, "L"], [0.03328, "e"], [0.049745, "t"], [0.090644, "'"], [0.035465, "s"], [0.029885, "\u001b[?25l\u001b[?12l\u001b[?25h"], [0.002888, " "], [0.024054, "f"], [0.015762, "i"], [0.031108, "n"], [0.113196, "d"], [0.060403, " "], [0.048114, "s"], [0.125049, "o"], [0.047089, "m"], [0.021108, "e"], [0.077087, " "], [0.036722, "n"], [0.069822, "e"], [0.104452, "w"], [0.014878, " "], [0.127773, "h"], [0.015065, "o"], [0.107634, "s"], [0.041584, "t"], [0.098004, "s"], [0.043394, "\u001b[125C\n\r"], [4.2e-05, "$ "], [0.025291, "s"], [0.051349, "s"], [0.125313, "h"], [0.056782, "-"], [0.074545, "a"], [0.112696, "u"], [0.125065, "d"], [0.066919, "i"], [0.1353, "t"], [0.102625, "o"], [0.111406, "r"], [0.052167, " "], [0.120379, "d"], [0.099012, "i"], [0.111665, "s"], [0.032258, "c"], [0.085271, "o"], [0.021735, "v"], [0.071485, "e"], [0.056875, "r"], [0.089216, " "], [0.085939, "1"], [0.018246, "9"], [0.029468, "2"], [0.066628, "."], [0.032987, "1"], [0.010485, "6"], [0.079394, "8"], [0.008837, "."], [0.061708, "2"], [0.132932, "."], [0.097158, "0"], [0.085731, "/"], [0.124356, "2"], [0.133735, "4"], [0.008781, " "], [0.122092, "-"], [0.129297, "p"], [0.045659, " "], [0.035321, "2"], [0.139604, "2"], [0.042856, "2"], [0.047636, "1"], [1.055567, "\u001b[109C\n\r"], [0.015462, "\u001b[154C\n\u001b[27;1H\u001b[32mINFO\u001b[39m[08-04|15:58:08] discovering hosts \u001b[32minclude\u001b[39m=192.168.2.0/24 \u001b[32mexclude\u001b[39m= \u001b[32mtotal\u001b[39m=256 \u001b[32mports\u001b[39m=2221\r\n"], [0.016479, "\u001b[154C\n\u001b[27;1H\u001b[32mINFO\u001b[39m[08-04|15:58:08] current known hosts \u001b[32mcount\u001b[39m=19\r\n"], [0.513707, "\u001b[?25l\u001b[?12l\u001b[?25h"], [1.92184, "\u001b[3S\u001b[3A\u001b[32mINFO\u001b[39m[08-04|15:58:10] discovered new host \u001b[32mhost\u001b[39m=192.168.2.230:2221 \u001b[32mversion\u001b[39m=\"SSH-2.0-OpenSSH_6.7p1 Debian-5+deb8u3\" \u001b[32mfp\u001b[39m=SHA256:x6IdP6QKdFP3xQKmfDBFRmVgsX1QJn5oda7zyyjsJdI\r\n\u001b[32mINFO\u001b[39m[08-04|15:58:10] discovery report \u001b[32mtotal\u001b[39m=1 \u001b[32mnew\u001b[39m=1 \u001b[32mupdated\u001b[39m=0\r\n"], [0.002351, "\u001b[154C\n\u001b[27;1H\u001b[32mINFO\u001b[39m[08-04|15:58:10] brute force queue size \u001b[32mnew\u001b[39m=3 \u001b[32mtotal\u001b[39m=3\r\n"], [0.005729, "$ "], [0.133604, "#"], [0.085936, " "], [0.050191, "B"], [0.067121, "e"], [0.104774, "f"], [0.083804, "\u001b[?25l\u001b[?12l\u001b[?25h"], [0.002909, "o"], [0.118356, "r"], [0.067927, "e"], [0.084049, " "], [0.087321, "w"], [0.033135, "e"], [0.016489, " "], [0.089554, "r"], [0.021123, "e"], [0.045611, "-"], [0.099155, "s"], [0.03511, "c"], [0.048103, "a"], [0.059476, "n"], [0.076156, ","], [0.089007, " "], [0.13221, "l"], [0.076591, "e"], [0.102252, "t"], [0.024294, "'"], [0.117484, "s"], [0.057528, " "], [0.051623, "a"], [0.095849, "d"], [0.031339, "d"], [0.118519, " "], [0.008863, "o"], [0.135547, "n"], [0.053604, "e"], [0.122505, " "], [0.066367, "m"], [0.05075, "o"], [0.067648, "r"], [0.0918, "e"], [0.067132, " "], [0.113006, "n"], [0.106826, "e"], [0.126937, "w"], [0.047479, " "], [0.089618, "c"], [0.07306, "r"], [0.054962, "e"], [0.092594, "d"], [0.054655, "e"], [0.081004, "n"], [0.100117, "t"], [0.09447, "i"], [0.061814, "a"], [0.027251, "l"], [0.093396, "\u001b[98C\n\r"], [4.2e-05, "$ "], [0.026041, "s"], [0.017675, "s"], [0.063458, "h"], [0.065108, "-"], [0.086917, "a"], [0.009098, "u"], [0.02147, "d"], [0.045018, "i"], [0.100114, "t"], [0.122603, "o"], [0.130501, "r"], [0.015453, " "], [0.04385, "a"], [0.065349, "c"], [0.128516, " "], [0.029564, "u"], [0.121665, "b"], [0.034998, "n"], [0.078164, "t"], [0.099892, " "], [0.073344, "u"], [0.078458, "b"], [0.075504, "n"], [0.008986, "t"], [0.017731, "\u001b[128C\n\r"], [0.02309, "\u001b[154C\n\u001b[27;1H\u001b[32mINFO\u001b[39m[08-04|15:58:16] added credential \u001b[32muser\u001b[39m=ubnt \u001b[32mpassword\u001b[39m=ubnt \u001b[32minterval\u001b[39m=14\r\n"], [0.000706, "$ "], [0.044678, "#"], [0.129009, " "], [0.087816, "N"], [0.055079, "o"], [0.047556, "w"], [0.045124, " "], [0.052966, "i"], [0.033982, "f"], [0.067087, " "], [0.076284, "w"], [0.076243, "e"], [0.108655, " "], [0.088244, "r"], [0.086861, "e"], [0.121858, "-"], [0.095713, "s"], [0.131591, "c"], [0.091882, "a"], [0.111299, "n"], [0.10226, ","], [0.084551, " "], [0.10214, "s"], [0.015168, "s"], [0.106579, "h"], [0.084708, "-"], [0.042334, "a"], [0.11036, "u"], [0.106953, "d"], [0.079527, "i"], [0.117682, "t"], [0.067289, "o"], [0.047921, "r"], [0.114241, " "], [0.053446, "w"], [0.065777, "i"], [0.134523, "l"], [0.041681, "l"], [0.056339, " "], [0.134342, "s"], [0.079583, "c"], [0.118992, "a"], [0.022529, "n"], [0.05136, " "], [0.125431, "t"], [0.033656, "h"], [0.132996, "e"], [0.047114, " "], [0.098529, "n"], [0.129524, "e"], [0.06854, "w"], [0.040165, " "], [0.10808, "c"], [0.066936, "r"], [0.033071, "e"], [0.030766, "d"], [0.069056, "e"], [0.054523, "n"], [0.090013, "t"], [0.050255, "i"], [0.077677, "a"], [0.066019, "l"], [0.07648, " "], [0.093916, "o"], [0.057879, "n"], [0.037167, " "], [0.097094, "a"], [0.13432, "l"], [0.089607, "l"], [0.078546, " "], [0.130727, "h"], [0.06491, "o"], [0.076207, "s"], [0.092325, "t"], [0.066818, "s"], [0.12248, "\u001b[78C\n\r"], [9.5e-05, "$ "], [0.008647, "#"], [0.04087, " "], [0.087411, "a"], [0.081599, "s"], [0.084527, " "], [0.024291, "w"], [0.049648, "e"], [0.135568, "l"], [0.089645, "l"], [0.037277, " "], [0.04671, "a"], [0.059211, "s"], [0.111624, " "], [0.121029, "s"], [0.109699, "c"], [0.033644, "a"], [0.030637, "n"], [0.021736, " "], [0.09649, "t"], [0.074475, "h"], [0.032595, "e"], [0.062281, " "], [0.088464, "n"], [0.0992, "e"], [0.073821, "w"], [0.043617, " "], [0.041557, "h"], [0.075771, "o"], [0.017425, "s"], [0.097036, "t"], [0.104, " "], [0.131413, "w"], [0.069972, "i"], [0.066036, "t"], [0.130894, "h"], [0.054509, " "], [0.027665, "a"], [0.067925, "l"], [0.131917, "l"], [0.13511, " "], [0.066579, "t"], [0.099919, "h"], [0.114625, "e"], [0.046946, " "], [0.132899, "o"], [0.056214, "l"], [0.102697, "d"], [0.023771, " "], [0.116513, "c"], [0.041248, "r"], [0.022893, "e"], [0.12935, "d"], [0.093616, "e"], [0.035858, "n"], [0.040565, "t"], [0.125884, "i"], [0.046429, "a"], [0.104598, "l"], [0.111324, "s"], [0.104513, "\u001b[93C\n\r"], [4.2e-05, "$ "], [0.02606, "s"], [0.024544, "s"], [0.083822, "h"], [0.131662, "-"], [0.06805, "a"], [0.138672, "u"], [0.030211, "d"], [0.069383, "i"], [0.099967, "t"], [0.02411, "o"], [0.028758, "r"], [0.060508, " "], [0.072637, "s"], [0.03304, "c"], [0.107675, "a"], [0.075035, "n"], [0.038896, " "], [0.043189, "-"], [0.104927, "-"], [0.132087, "d"], [0.015154, "e"], [0.135588, "b"], [0.022295, "u"], [0.027676, "g"], [0.107279, "\u001b[128C\n\r"], [0.006262, "\u001b[?25l\u001b[?12l\u001b[?25h"], [0.010778, "\u001b[154C\n\u001b[27;1H\u001b[32mINFO\u001b[39m[08-04|15:58:28] brute force queue size \u001b[32mnew\u001b[39m=20 \u001b[32mtotal\u001b[39m=23\r\n"], [0.052015, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:58:28] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.230:22 \u001b[36muser\u001b[39m=ubnt \u001b[36mpassword\u001b[39m=ubnt \u001b[36mresult\u001b[39m=\r\n"], [0.037977, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:58:28] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.173:22 \u001b[36muser\u001b[39m=ubnt \u001b[36mpassword\u001b[39m=ubnt \u001b[36mresult\u001b[39m=\r\n"], [0.00353, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:58:28] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.25:22 \u001b[36muser\u001b[39m=ubnt \u001b[36mpassword\u001b[39m=ubnt \u001b[36mresult\u001b[39m=\r\n"], [0.008989, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:58:28] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.224:22 \u001b[36muser\u001b[39m=ubnt \u001b[36mpassword\u001b[39m=ubnt \u001b[36mresult\u001b[39m=\r\n"], [0.042219, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:58:28] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.20:22 \u001b[36muser\u001b[39m=ubnt \u001b[36mpassword\u001b[39m=ubnt \u001b[36mresult\u001b[39m=\r\n"], [0.019049, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:58:28] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.23:22 \u001b[36muser\u001b[39m=ubnt \u001b[36mpassword\u001b[39m=ubnt \u001b[36mresult\u001b[39m=\r\n"], [0.003737, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:58:28] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.11:22 \u001b[36muser\u001b[39m=ubnt \u001b[36mpassword\u001b[39m=ubnt \u001b[36mresult\u001b[39m=\r\n"], [0.238916, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:58:29] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.166:22 \u001b[36muser\u001b[39m=ubnt \u001b[36mpassword\u001b[39m=ubnt \u001b[36mresult\u001b[39m=\r\n"], [0.677247, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:58:29] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.243:22 \u001b[36muser\u001b[39m=ubnt \u001b[36mpassword\u001b[39m=ubnt \u001b[36mresult\u001b[39m=\r\n"], [1.63865, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:58:31] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.157:22 \u001b[36muser\u001b[39m=ubnt \u001b[36mpassword\u001b[39m=ubnt \u001b[36mresult\u001b[39m=\r\n"], [0.013898, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:58:31] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.179:22 \u001b[36muser\u001b[39m=ubnt \u001b[36mpassword\u001b[39m=ubnt \u001b[36mresult\u001b[39m=\r\n"], [0.009847, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:58:31] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.21:22 \u001b[36muser\u001b[39m=ubnt \u001b[36mpassword\u001b[39m=ubnt \u001b[36mresult\u001b[39m=\r\n"], [0.000574, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:58:31] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.22:22 \u001b[36muser\u001b[39m=ubnt \u001b[36mpassword\u001b[39m=ubnt \u001b[36mresult\u001b[39m=\r\n"], [0.004373, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:58:31] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.238:22 \u001b[36muser\u001b[39m=ubnt \u001b[36mpassword\u001b[39m=ubnt \u001b[36mresult\u001b[39m=\r\n"], [0.014246, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:58:31] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.230:2221 \u001b[36muser\u001b[39m=root \u001b[36mpassword\u001b[39m=root \u001b[36mresult\u001b[39m=\r\n"], [0.045127, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:58:31] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.182:22 \u001b[36muser\u001b[39m=ubnt \u001b[36mpassword\u001b[39m=ubnt \u001b[36mresult\u001b[39m=\r\n"], [0.006792, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:58:31] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.181:22 \u001b[36muser\u001b[39m=ubnt \u001b[36mpassword\u001b[39m=ubnt \u001b[36mresult\u001b[39m=\r\n"], [0.007826, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:58:31] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.183:22 \u001b[36muser\u001b[39m=ubnt \u001b[36mpassword\u001b[39m=ubnt \u001b[36mresult\u001b[39m=\r\n"], [0.207756, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:58:31] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.1:22 \u001b[36muser\u001b[39m=ubnt \u001b[36mpassword\u001b[39m=ubnt \u001b[36mresult\u001b[39m=\r\n"], [1.562214, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:58:33] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.230:2221 \u001b[36muser\u001b[39m=admin \u001b[36mpassword\u001b[39m=admin \u001b[36mresult\u001b[39m=\r\n"], [2.069724, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:58:35] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.230:2221 \u001b[36muser\u001b[39m=test \u001b[36mpassword\u001b[39m=test \u001b[36mresult\u001b[39m=\r\n"], [1.347047, "\u001b[2S\u001b[2A\u001b[31mEROR\u001b[39m[08-04|15:58:36] brute force error \u001b[31mhost\u001b[39m=192.168.2.10:22 \u001b[31muser\u001b[39m=ubnt \u001b[31mpassword\u001b[39m=ubnt \u001b[31mresult\u001b[39m= \u001b[31merr\u001b[39m=\"ssh: handshake failed: read tcp 192.168.2.144:58390->192.168.2.10:22: i/o timeout\"\r\n"], [0.999686, "\u001b[154C\n\u001b[27;1H\u001b[36mDBUG\u001b[39m[08-04|15:58:37] negative brute force result \u001b[36mhost\u001b[39m=192.168.2.230:2221 \u001b[36muser\u001b[39m=ubnt \u001b[36mpassword\u001b[39m=ubnt \u001b[36mresult\u001b[39m=\r\n"], [5.4e-05, "\u001b[154C\n\u001b[27;1H\u001b[32mINFO\u001b[39m[08-04|15:58:37] brute force scan report \u001b[32mtotal\u001b[39m=23 \u001b[32mneg\u001b[39m=22 \u001b[32mpos\u001b[39m=0 \u001b[32merr\u001b[39m=1\r\n"], [0.003449, "$ "], [0.133557, "#"], [0.134559, " "], [0.096952, "W"], [0.088461, "h"], [0.092506, "\u001b[?25l\u001b[?12l\u001b[?25h"], [0.002837, "e"], [0.060882, "n"], [0.059196, " "], [0.058738, "t"], [0.023158, "h"], [0.094533, "e"], [0.04837, " "], [0.045209, "o"], [0.03051, "u"], [0.116457, "t"], [0.135827, "p"], [0.051348, "u"], [0.043059, "t"], [0.047743, " "], [0.080792, "i"], [0.134393, "s"], [0.128462, " "], [0.127156, "t"], [0.098737, "o"], [0.104708, " "], [0.126856, "a"], [0.077241, " "], [0.0525, "f"], [0.062474, "i"], [0.107891, "l"], [0.087169, "e"], [0.046326, ","], [0.059755, " "], [0.062274, "l"], [0.050195, "o"], [0.114109, "g"], [0.059807, "s"], [0.050528, " "], [0.04694, "a"], [0.082191, "r"], [0.053224, "e"], [0.068145, " "], [0.028825, "m"], [0.082024, "a"], [0.134772, "c"], [0.043634, "h"], [0.079731, "i"], [0.05168, "n"], [0.025251, "e"], [0.074933, " "], [0.105128, "r"], [0.018289, "e"], [0.112127, "a"], [0.070868, "d"], [0.064494, "a"], [0.130713, "b"], [0.032626, "l"], [0.132757, "e"], [0.100604, "\u001b[95C\n\r"], [8.6e-05, "$ "], [0.026966, "s"], [0.040374, "s"], [0.081922, "h"], [0.128521, "-"], [0.055206, "a"], [0.060671, "u"], [0.09711, "d"], [0.09491, "i"], [0.078967, "t"], [0.136808, "o"], [0.07772, "r"], [0.121051, " "], [0.053952, "d"], [0.024436, "i"], [0.131772, "s"], [0.064328, "c"], [0.03139, "o"], [0.091578, "v"], [0.103564, "e"], [0.015017, "r"], [0.102652, " "], [0.059216, "1"], [0.094864, "9"], [0.097657, "2"], [0.04776, "."], [0.072478, "1"], [0.052777, "6"], [0.111132, "8"], [0.100118, "."], [0.131734, "2"], [0.04462, "."], [0.127232, "0"], [0.110543, "/"], [0.036732, "2"], [0.069817, "4"], [0.075434, " "], [0.102143, "-"], [0.056695, "p"], [0.106168, " "], [0.076369, "2"], [0.06706, "2"], [0.089575, "2"], [0.018035, "1"], [0.101297, " "], [0.02127, "&"], [0.037118, ">"], [0.071956, " "], [0.092117, "s"], [0.123182, "s"], [0.104645, "h"], [0.073909, "."], [0.017181, "l"], [0.083206, "o"], [0.11462, "g"], [1.022988, "\u001b[98C\n\r"], [2.439618, "$ "], [0.058295, "c"], [0.086744, "a"], [0.135649, "t"], [0.077341, " "], [0.043919, "s"], [0.126841, "s"], [0.051943, "h"], [0.021907, "."], [0.131928, "l"], [0.067927, "o"], [0.135927, "g"], [0.105837, "\u001b[141C\n\r"], [0.004175, "\u001b[4S\u001b[4At=2017-08-04T15:58:47-0400 lvl=info msg=\"discovering hosts\" include=192.168.2.0/24 exclude= total=256 ports=2221\r\nt=2017-08-04T15:58:47-0400 lvl=info msg=\"current known hosts\" count=20\r\nt=2017-08-04T15:58:50-0400 lvl=info msg=\"discovery report\" total=1 new=0 updated=0\r\nt=2017-08-04T15:58:50-0400 lvl=info msg=\"brute force queue size\" new=0 total=0\r\n"], [0.00047, "$ "], [0.048488, "#"], [0.070065, " "], [0.108377, "T"], [0.100654, "h"], [0.100442, "a"], [0.104143, "t"], [0.125721, "'"], [0.135356, "s"], [0.009147, " "], [0.114474, "i"], [0.037645, "t"], [0.049175, "!"], [0.121052, "\u001b[140C\n\r"], [0.000114, "$ "]], "height": 28, "width": 155, "version": 1, "command": "tmux attach -t howdy", "env": {"TERM": "xterm-256color", "SHELL": "/bin/bash"}, "duration": 160.532531} -------------------------------------------------------------------------------- /demo/intro.yaml: -------------------------------------------------------------------------------- 1 | title: ssh-auditor 2 | before: rm -f ssh_db.sqlite && export PS1='$ ' && clear 3 | scenes: 4 | - name: started 5 | line: Let's get started! 6 | - name: install 7 | action: go get github.com/ncsa/ssh-auditor 8 | wait: true 9 | - name: 10 | line: Ensure our path is set properly. GOPATH is normally ~/go/ 11 | - name: set path 12 | action: PATH=$PATH:$(go env GOPATH) 13 | wait: true 14 | - name: show help 15 | action: ssh-auditor 16 | hesitate: 2 17 | wait: true 18 | - name: it works 19 | line: It looks like it's working! 20 | - name: pause 21 | pause: 1 22 | 23 | - name: Discover some hosts 24 | line: First we need to discover hosts running sshd 25 | - name: discover 26 | action: ssh-auditor discover 192.168.2.0/24 27 | hesitate: 2 28 | wait: true 29 | - name: Scan? 30 | line: Now that we have some hosts, can we scan then? 31 | - name: scan? 32 | action: ssh-auditor scan --debug 33 | wait: true 34 | - name: not yet! 35 | line: Not yet! Before we can scan the hosts, we need credentials 36 | - name: add some credentials 37 | line: Let's add some! 38 | - name: pause 39 | pause: 1 40 | 41 | - name: ac 42 | action: ssh-auditor addcredential root root 43 | hesitate: 1 44 | wait: true 45 | - name: alias 46 | line: ac is an alias for addcredential 47 | - name: ac 48 | action: ssh-auditor ac admin admin 49 | wait: true 50 | 51 | - name: Scan? 52 | line: Now that we have credentials, can we scan the hosts now? 53 | - name: scan? 54 | action: ssh-auditor scan --debug 55 | hesitate: 2 56 | wait: true 57 | - name: vuln? 58 | action: ssh-auditor vuln 59 | hesitate: 2 60 | wait: true 61 | - name: good 62 | line: Nothing vulnerable, so far so good 63 | 64 | - name: one more 65 | line: If we add a new credential, ssh-auditor will scan all known hosts with that new credential 66 | - name: ac 67 | action: ssh-auditor ac test test 68 | wait: true 69 | - name: scan? 70 | action: ssh-auditor scan --debug 71 | wait: true 72 | 73 | - name: more hosts 74 | line: Let's find some new hosts 75 | - name: discover again 76 | action: ssh-auditor discover 192.168.2.0/24 -p 2221 77 | hesitate: 1 78 | wait: true 79 | - name: again! 80 | line: Before we re-scan, let's add one more new credential 81 | - name: ac ubnt ubnt 82 | action: ssh-auditor ac ubnt ubnt 83 | wait: true 84 | - name: and rescan 85 | line: Now if we re-scan, ssh-auditor will scan the new credential on all hosts 86 | - name: .. 87 | line: as well as scan the new host with all the old credentials 88 | - name: scan? 89 | action: ssh-auditor scan --debug 90 | wait: true 91 | 92 | - name: log 93 | line: When the output is to a file, logs are machine readable 94 | - name: discover to a log 95 | action: ssh-auditor discover 192.168.2.0/24 -p 2221 &> ssh.log 96 | hesitate: 1 97 | wait: true 98 | - name: show log 99 | action: cat ssh.log 100 | wait: true 101 | 102 | - name: end 103 | line: That's it! 104 | - name: pause 105 | pause: 5 106 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | alpine-sshd-ok: 5 | build: testing/docker/alpine-sshd-ok 6 | alpine-sshd-test-test: 7 | build: testing/docker/alpine-sshd-test-test 8 | alpine-sshd-test-key: 9 | build: testing/docker/alpine-sshd-test-key 10 | alpine-sshd-test-blank: 11 | build: testing/docker/alpine-sshd-test-blank 12 | alpine-sshd-test-test-no-id-binary: 13 | build: testing/docker/alpine-sshd-test-test-no-id-binary 14 | alpine-sshd-test-test-no-id-binary-tunnel-local: 15 | build: testing/docker/alpine-sshd-test-test-no-id-binary-tunnel-local 16 | alpine-sshd-test-test-no-id-binary-no-tunnel: 17 | build: testing/docker/alpine-sshd-test-test-no-id-binary-no-tunnel 18 | auditor: 19 | build: . 20 | command: go test ./... 21 | volumes: 22 | - .:/go/src/github.com/ncsa/ssh-auditor 23 | depends_on: 24 | - alpine-sshd-ok 25 | - alpine-sshd-test-test 26 | - alpine-sshd-test-key 27 | - alpine-sshd-test-blank 28 | - alpine-sshd-test-test-no-id-binary 29 | - alpine-sshd-test-test-no-id-binary-tunnel-local 30 | - alpine-sshd-test-test-no-id-binary-no-tunnel 31 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ncsa/ssh-auditor 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/inconshreveable/log15 v0.0.0-20200109203555-b30bc20e4fd1 7 | github.com/jmoiron/sqlx v1.2.0 8 | github.com/mattn/go-colorable v0.1.4 // indirect 9 | github.com/mattn/go-isatty v0.0.12 // indirect 10 | github.com/mattn/go-sqlite3 v2.0.3+incompatible 11 | github.com/pkg/errors v0.9.1 12 | github.com/sebkl/splunk-golang v0.0.0-20151111121930-5ea88f4c7e42 13 | github.com/spf13/cobra v0.0.6 14 | golang.org/x/crypto v0.0.0-20200219234226-1ad67e1f0ef4 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 4 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 5 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 6 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 7 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 8 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 9 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 10 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 11 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 12 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 13 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 14 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 15 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 16 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 17 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 19 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 20 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 21 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 22 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 23 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 24 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 25 | github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 26 | github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= 27 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 28 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 29 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 30 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 31 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 32 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 33 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 34 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 35 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 36 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 37 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 38 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 39 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 40 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 41 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 42 | github.com/inconshreveable/log15 v0.0.0-20200109203555-b30bc20e4fd1 h1:KUDFlmBg2buRWNzIcwLlKvfcnujcHQRQ1As1LoaCLAM= 43 | github.com/inconshreveable/log15 v0.0.0-20200109203555-b30bc20e4fd1/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o= 44 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 45 | github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= 46 | github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= 47 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 48 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 49 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 50 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 51 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 52 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 53 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 54 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 55 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 56 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 57 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 58 | github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= 59 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 60 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 61 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 62 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 63 | github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 64 | github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= 65 | github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 66 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 67 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 68 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 69 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 70 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 71 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 72 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 73 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 74 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 75 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 76 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 77 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 78 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 79 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 80 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 81 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 82 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 83 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 84 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 85 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 86 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 87 | github.com/sebkl/splunk-golang v0.0.0-20151111121930-5ea88f4c7e42 h1:vLfdc1U7h8IUmecasiUy9/XLUStinYVWiBBNPXuTqKE= 88 | github.com/sebkl/splunk-golang v0.0.0-20151111121930-5ea88f4c7e42/go.mod h1:XOAaEUTFoTVFB2b+5idGpqweLI0Ld8NTruH8+l6uIO8= 89 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 90 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 91 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 92 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 93 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 94 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 95 | github.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs= 96 | github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= 97 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 98 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 99 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 100 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= 101 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 102 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 103 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 104 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 105 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 106 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 107 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 108 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 109 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 110 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 111 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 112 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 113 | golang.org/x/crypto v0.0.0-20200219234226-1ad67e1f0ef4 h1:4icQlpeqbz3WxfgP6Eq3szTj95KTrlH/CwzBzoxuFd0= 114 | golang.org/x/crypto v0.0.0-20200219234226-1ad67e1f0ef4/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 115 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 116 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 117 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 118 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 119 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 120 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 121 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 122 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 123 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 124 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 125 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 126 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 127 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 128 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 129 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 130 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 131 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 132 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 133 | golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= 134 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 135 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= 136 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 137 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 138 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 139 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 140 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 141 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 142 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 143 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 144 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 145 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 146 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 147 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 148 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 149 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 150 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 151 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 152 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 153 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 154 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | log "github.com/inconshreveable/log15" 7 | "github.com/ncsa/ssh-auditor/cmd" 8 | ) 9 | 10 | func main() { 11 | if err := cmd.RootCmd.Execute(); err != nil { 12 | log.Error(err.Error()) 13 | os.Exit(1) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /sshauditor/auditor.go: -------------------------------------------------------------------------------- 1 | package sshauditor 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | log "github.com/inconshreveable/log15" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | type ScanConfiguration struct { 16 | Include []string 17 | Exclude []string 18 | Ports []int 19 | Concurrency int 20 | } 21 | type AuditResult struct { 22 | totalCount int 23 | negCount int 24 | posCount int 25 | errCount int 26 | } 27 | type AuditReport struct { 28 | ActiveHosts []Host 29 | ActiveHostsCount int 30 | 31 | DuplicateKeys map[string][]Host 32 | DuplicateKeysCount int 33 | 34 | Vulnerabilities []Vulnerability 35 | VulnerabilitiesCount int 36 | } 37 | 38 | func joinInts(ints []int, sep string) string { 39 | var foo []string 40 | for _, i := range ints { 41 | foo = append(foo, strconv.Itoa(i)) 42 | } 43 | return strings.Join(foo, sep) 44 | } 45 | 46 | //expandScanConfiguration takes a ScanConfiguration and returns a channel 47 | //of all hostports that match the scan configuration. 48 | func expandScanConfiguration(cfg ScanConfiguration) (chan string, error) { 49 | hostChan := make(chan string, 1024) 50 | hosts, err := EnumerateHosts(cfg.Include, cfg.Exclude) 51 | if err != nil { 52 | return hostChan, err 53 | } 54 | log.Info("discovering hosts", 55 | "include", strings.Join(cfg.Include, ","), 56 | "exclude", strings.Join(cfg.Exclude, ","), 57 | "total", len(hosts), 58 | "ports", joinInts(cfg.Ports, ","), 59 | ) 60 | go func() { 61 | // Iterate over ports first, so for a large scan there's a 62 | // delay between attempts per host 63 | for _, port := range cfg.Ports { 64 | portString := strconv.Itoa(port) 65 | for _, h := range hosts { 66 | hostChan <- net.JoinHostPort(h, portString) 67 | } 68 | } 69 | close(hostChan) 70 | }() 71 | return hostChan, err 72 | } 73 | 74 | type SSHAuditor struct { 75 | //TODO: should be interface 76 | store *SQLiteStore 77 | } 78 | 79 | func New(store *SQLiteStore) *SSHAuditor { 80 | return &SSHAuditor{ 81 | store: store, 82 | } 83 | } 84 | 85 | func (a *SSHAuditor) updateStoreFromDiscovery(hosts chan SSHHost) error { 86 | knownHosts, err := a.store.getKnownHosts() 87 | if err != nil { 88 | return err 89 | } 90 | log.Info("current known hosts", "count", len(knownHosts)) 91 | var totalCount, updatedCount, newCount int 92 | 93 | hostsWrapped := make(chan interface{}) 94 | go func() { 95 | for v := range hosts { 96 | hostsWrapped <- v 97 | } 98 | close(hostsWrapped) 99 | }() 100 | for hostBatch := range batch(context.TODO(), hostsWrapped, 50, 2*time.Second) { 101 | _, err := a.store.Begin() 102 | if err != nil { 103 | return errors.Wrap(err, "updateStoreFromDiscovery") 104 | } 105 | for _, host := range hostBatch { 106 | host := host.(SSHHost) 107 | var needUpdate bool 108 | rec, existing := knownHosts[host.hostport] 109 | if existing { 110 | if host.keyfp == "" { 111 | host.keyfp = rec.Fingerprint 112 | } 113 | if host.version == "" { 114 | host.version = rec.Version 115 | } 116 | needUpdate = (host.keyfp != rec.Fingerprint || host.version != rec.Version) 117 | err := a.store.addHostChanges(host, rec) 118 | if err != nil { 119 | return errors.Wrap(err, "updateStoreFromDiscovery") 120 | } 121 | } 122 | l := log.New("host", host.hostport, "version", host.version, "fp", host.keyfp) 123 | if !existing || needUpdate { 124 | err = a.store.addOrUpdateHost(host) 125 | if err != nil { 126 | return errors.Wrap(err, "updateStoreFromDiscovery") 127 | } 128 | } 129 | //If it already existed and we didn't otherwise update it, mark that it was seen 130 | if existing { 131 | err = a.store.setLastSeen(host) 132 | if err != nil { 133 | return errors.Wrap(err, "updateStoreFromDiscovery") 134 | } 135 | } 136 | totalCount++ 137 | if !existing { 138 | l.Info("discovered new host") 139 | newCount++ 140 | } else if needUpdate { 141 | l.Info("discovered changed host") 142 | updatedCount++ 143 | } 144 | } 145 | err = a.store.Commit() 146 | if err != nil { 147 | return errors.Wrap(err, "updateStoreFromDiscovery") 148 | } 149 | } 150 | log.Info("discovery report", "total", totalCount, "new", newCount, "updated", updatedCount) 151 | return nil 152 | } 153 | 154 | func (a *SSHAuditor) updateQueues() error { 155 | queued, err := a.store.initHostCreds() 156 | if err != nil { 157 | return err 158 | } 159 | queuesize, err := a.store.getScanQueueSize() 160 | if err != nil { 161 | return err 162 | } 163 | log.Info("brute force queue size", "new", queued, "total", queuesize) 164 | return nil 165 | } 166 | 167 | func (a *SSHAuditor) Discover(cfg ScanConfiguration) error { 168 | //Push all candidate hosts into the banner fetcher queue 169 | hostChan, err := expandScanConfiguration(cfg) 170 | if err != nil { 171 | return err 172 | } 173 | 174 | portResults := bannerFetcher(cfg.Concurrency*2, hostChan) 175 | keyResults := fingerPrintFetcher(cfg.Concurrency, portResults) 176 | 177 | err = a.updateStoreFromDiscovery(keyResults) 178 | if err != nil { 179 | return err 180 | } 181 | 182 | err = a.updateQueues() 183 | return err 184 | } 185 | 186 | func (a *SSHAuditor) brute(scantype string, cfg ScanConfiguration) (AuditResult, error) { 187 | var res AuditResult 188 | a.updateQueues() 189 | var err error 190 | 191 | var sc []ScanRequest 192 | switch scantype { 193 | case "scan": 194 | sc, err = a.store.getScanQueue() 195 | case "rescan": 196 | sc, err = a.store.getRescanQueue() 197 | } 198 | if err != nil { 199 | return res, errors.Wrap(err, "Error getting scan queue") 200 | } 201 | bruteResults := bruteForcer(cfg.Concurrency, sc) 202 | 203 | bruteResultsWrapped := make(chan interface{}) 204 | go func() { 205 | for v := range bruteResults { 206 | bruteResultsWrapped <- v 207 | } 208 | close(bruteResultsWrapped) 209 | }() 210 | 211 | var totalCount, errCount, negCount, posCount int 212 | for bruteBatch := range batch(context.TODO(), bruteResultsWrapped, 50, 2*time.Second) { 213 | _, err = a.store.Begin() 214 | if err != nil { 215 | return res, errors.Wrap(err, "brute") 216 | } 217 | 218 | for _, br := range bruteBatch { 219 | br := br.(BruteForceResult) 220 | l := log.New( 221 | "host", br.hostport, 222 | "user", br.cred.User, 223 | "password", br.cred.Password, 224 | "result", br.result, 225 | ) 226 | if br.err != nil { 227 | l.Error("brute force error", "err", br.err.Error()) 228 | errCount++ 229 | } else if br.result == "" { 230 | l.Debug("negative brute force result") 231 | negCount++ 232 | } else { 233 | l.Info("positive brute force result") 234 | posCount++ 235 | } 236 | err = a.store.updateBruteResult(br) 237 | if err != nil { 238 | return res, err 239 | } 240 | totalCount++ 241 | } 242 | err = a.store.Commit() 243 | if err != nil { 244 | return res, errors.Wrap(err, "brute") 245 | } 246 | } 247 | log.Info("brute force scan report", "total", totalCount, "neg", negCount, "pos", posCount, "err", errCount) 248 | return AuditResult{ 249 | totalCount: totalCount, 250 | negCount: negCount, 251 | posCount: posCount, 252 | errCount: errCount, 253 | }, nil 254 | } 255 | 256 | func (a *SSHAuditor) Scan(cfg ScanConfiguration) (AuditResult, error) { 257 | return a.brute("scan", cfg) 258 | } 259 | func (a *SSHAuditor) Rescan(cfg ScanConfiguration) (AuditResult, error) { 260 | return a.brute("rescan", cfg) 261 | } 262 | 263 | func (a *SSHAuditor) Dupes() (map[string][]Host, error) { 264 | keyMap := make(map[string][]Host) 265 | 266 | hosts, err := a.store.GetActiveHosts(2) 267 | 268 | if err != nil { 269 | return keyMap, errors.Wrap(err, "Dupes") 270 | } 271 | 272 | for _, h := range hosts { 273 | keyMap[h.Fingerprint] = append(keyMap[h.Fingerprint], h) 274 | } 275 | 276 | for fp, hosts := range keyMap { 277 | if len(hosts) == 1 { 278 | delete(keyMap, fp) 279 | } 280 | } 281 | return keyMap, nil 282 | } 283 | func (a *SSHAuditor) getLogCheckScanQueue() ([]ScanRequest, error) { 284 | var requests []ScanRequest 285 | hostList, err := a.store.GetActiveHosts(14) 286 | if err != nil { 287 | return requests, errors.Wrap(err, "getLogCheckQueue") 288 | } 289 | 290 | for _, h := range hostList { 291 | host, _, err := net.SplitHostPort(h.Hostport) 292 | if err != nil { 293 | log.Warn("bad hostport? %s %s", h.Hostport, err) 294 | continue 295 | } 296 | user := fmt.Sprintf("logcheck-%s", host) 297 | 298 | sr := ScanRequest{ 299 | hostport: h.Hostport, 300 | credentials: []Credential{{User: user, Password: "logcheck"}}, 301 | } 302 | requests = append(requests, sr) 303 | } 304 | 305 | return requests, nil 306 | } 307 | 308 | func (a *SSHAuditor) Logcheck(cfg ScanConfiguration) error { 309 | sc, err := a.getLogCheckScanQueue() 310 | if err != nil { 311 | return err 312 | } 313 | 314 | bruteResults := bruteForcer(cfg.Concurrency, sc) 315 | 316 | for br := range bruteResults { 317 | l := log.New("host", br.hostport, "user", br.cred.User) 318 | if br.err != nil { 319 | l.Error("Failed to send logcheck auth request", "error", br.err) 320 | continue 321 | } 322 | l.Info("Sent logcheck auth request") 323 | //TODO Collect hostports and return them for syslog cross referencing 324 | } 325 | return nil 326 | } 327 | 328 | func (a *SSHAuditor) LogcheckReport(ls LogSearcher) error { 329 | activeHosts, err := a.store.GetActiveHosts(2) 330 | if err != nil { 331 | return errors.Wrap(err, "LogcheckReport GetActiveHosts failed") 332 | } 333 | 334 | foundIPs, err := ls.GetIPs() 335 | if err != nil { 336 | return errors.Wrap(err, "LogcheckReport GetIPs failed") 337 | } 338 | 339 | logPresent := make(map[string]bool) 340 | for _, host := range foundIPs { 341 | logPresent[host] = true 342 | } 343 | 344 | log.Info("found active hosts in store", "count", len(activeHosts)) 345 | log.Info("found related hosts in logs", "count", len(foundIPs)) 346 | 347 | for _, host := range activeHosts { 348 | ip, _, err := net.SplitHostPort(host.Hostport) 349 | if err != nil { 350 | log.Error("invalid hostport", "host", host.Hostport) 351 | continue 352 | } 353 | fmt.Printf("%s %v\n", host.Hostport, logPresent[ip]) 354 | } 355 | return nil 356 | } 357 | 358 | func (a *SSHAuditor) Vulnerabilities() ([]Vulnerability, error) { 359 | return a.store.GetVulnerabilities() 360 | } 361 | 362 | func (a *SSHAuditor) GetReport() (AuditReport, error) { 363 | var rep AuditReport 364 | hosts, err := a.store.GetActiveHosts(2) 365 | rep.ActiveHosts = hosts 366 | rep.ActiveHostsCount = len(hosts) 367 | if err != nil { 368 | return rep, err 369 | } 370 | 371 | dupes, err := a.Dupes() 372 | if err != nil { 373 | return rep, err 374 | } 375 | rep.DuplicateKeys = dupes 376 | rep.DuplicateKeysCount = len(dupes) 377 | 378 | vulns, err := a.Vulnerabilities() 379 | if err != nil { 380 | return rep, err 381 | } 382 | rep.Vulnerabilities = vulns 383 | rep.VulnerabilitiesCount = len(vulns) 384 | 385 | return rep, nil 386 | } 387 | -------------------------------------------------------------------------------- /sshauditor/auditor_test.go: -------------------------------------------------------------------------------- 1 | package sshauditor 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strconv" 7 | "testing" 8 | ) 9 | 10 | //makeScanConfig returns a ScanConfiguration based on a single host:port 11 | //Normally ssh-auditor deals with a list of cidr ranges and a list of ports 12 | //so this just needs to reformat things a bit for testing purposes so 13 | //things can be tested without knowing the ip range that docker is allocating 14 | func makeScanConfig(hostport string) (ScanConfiguration, string, error) { 15 | var sc ScanConfiguration 16 | host, port, err := net.SplitHostPort(hostport) 17 | //I control the test data, so this should not happen 18 | if err != nil { 19 | return sc, "", err 20 | } 21 | ips, err := net.LookupHost(host) 22 | if len(ips) != 1 { 23 | return sc, "", fmt.Errorf("Resolving of %s failed to return a sigle ip: %#v", host, ips) 24 | } 25 | scanDestination := fmt.Sprintf("%s/32", ips[0]) 26 | ipport := fmt.Sprintf("%s:%s", ips[0], port) 27 | portInt, err := strconv.Atoi(port) 28 | if err != nil { 29 | return sc, "", err 30 | } 31 | scanConfig := ScanConfiguration{ 32 | Concurrency: 1, 33 | Include: []string{scanDestination}, 34 | Ports: []int{portInt}, 35 | } 36 | return scanConfig, ipport, nil 37 | } 38 | 39 | //TestSSHAuditorE2E tests the discovery, scan, and vuln process 40 | //This re-uses the authTestCases from sshutil_test.go 41 | func TestSSHAuditorE2E(t *testing.T) { 42 | if testing.Short() { 43 | t.Skip("skipping test in short mode.") 44 | } 45 | for _, tt := range authTestCases { 46 | t.Run(fmt.Sprintf("TestSSHAuditorE2E(%q, %q, %q) => %q", tt.hostport, tt.user, tt.password, tt.expected), func(t *testing.T) { 47 | store, err := NewSQLiteStore(":memory:") 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | err = store.Init() 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | 56 | cred := Credential{ 57 | User: tt.user, 58 | Password: tt.password, 59 | ScanInterval: 1, 60 | } 61 | _, err = store.AddCredential(cred) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | 66 | auditor := New(store) 67 | sc, ipport, err := makeScanConfig(tt.hostport) 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | err = auditor.Discover(sc) 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | ar, err := auditor.Scan(sc) 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | if ar.totalCount != 1 { 80 | t.Errorf("totalCount != 1: %#v", ar.totalCount) 81 | } 82 | vulns, err := auditor.Vulnerabilities() 83 | if err != nil { 84 | t.Fatal(err) 85 | } 86 | 87 | if tt.expected != "" { 88 | if ar.posCount != 1 { 89 | t.Errorf("posCount != 1: %#v", ar.posCount) 90 | } 91 | if ar.negCount != 0 { 92 | t.Errorf("negCount != 0: %#v", ar.negCount) 93 | } 94 | if len(vulns) != 1 { 95 | t.Fatalf("len(vulns) != 1: %#v", vulns) 96 | } 97 | if vulns[0].Host.Hostport != ipport { 98 | t.Errorf("vuln[0].hostport != %#v: %#v", ipport, vulns) 99 | } 100 | if vulns[0].HostCredential.Result != tt.expected { 101 | t.Errorf("vuln[0].HostCredential.Result != %#v: %#v", tt.expected, vulns) 102 | } 103 | } else { 104 | if ar.posCount != 0 { 105 | t.Errorf("posCount != 0: %#v", ar.posCount) 106 | } 107 | if ar.negCount != 1 { 108 | t.Errorf("negCount != 1: %#v", ar.negCount) 109 | } 110 | if len(vulns) != 0 { 111 | t.Errorf("len(vulns) != 0: %#v", vulns) 112 | } 113 | } 114 | }) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /sshauditor/banner.go: -------------------------------------------------------------------------------- 1 | package sshauditor 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | type ScanResult struct { 10 | hostport string 11 | success bool 12 | banner string 13 | } 14 | 15 | func ScanPort(hostport string) ScanResult { 16 | res := ScanResult{hostport: hostport} 17 | var banner string 18 | conn, err := net.DialTimeout("tcp", hostport, 2*time.Second) 19 | if err != nil { 20 | return res 21 | } 22 | defer conn.Close() 23 | bannerBuffer := make([]byte, 256) 24 | conn.SetDeadline(time.Now().Add(4 * time.Second)) 25 | n, err := conn.Read(bannerBuffer) 26 | if err == nil { 27 | banner = string(bannerBuffer[:n]) 28 | 29 | newlinePosition := strings.IndexAny(banner, "\r\n") 30 | if newlinePosition != -1 { 31 | banner = banner[:newlinePosition] 32 | } 33 | } 34 | res.success = true 35 | res.banner = banner 36 | return res 37 | } 38 | -------------------------------------------------------------------------------- /sshauditor/bannerworker.go: -------------------------------------------------------------------------------- 1 | package sshauditor 2 | 3 | import "sync" 4 | 5 | func bannerWorker(jobs <-chan string, results chan<- ScanResult) { 6 | for host := range jobs { 7 | results <- ScanPort(host) 8 | } 9 | } 10 | 11 | func bannerFetcher(numWorkers int, hostports <-chan string) chan ScanResult { 12 | var wg sync.WaitGroup 13 | 14 | results := make(chan ScanResult, 1024) 15 | 16 | for w := 0; w <= numWorkers; w++ { 17 | wg.Add(1) 18 | go func() { 19 | bannerWorker(hostports, results) 20 | wg.Done() 21 | }() 22 | } 23 | 24 | go func() { 25 | wg.Wait() 26 | close(results) 27 | }() 28 | 29 | return results 30 | } 31 | -------------------------------------------------------------------------------- /sshauditor/batch.go: -------------------------------------------------------------------------------- 1 | package sshauditor 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | func batch(ctx context.Context, c chan interface{}, size int, maxInterval time.Duration) chan []interface{} { 9 | oc := make(chan []interface{}, 100) 10 | go func() { 11 | defer close(oc) 12 | var i int 13 | xs := make([]interface{}, 0, size) 14 | lastOutput := time.Now() 15 | for { 16 | select { 17 | case x, ok := <-c: 18 | if !ok { 19 | goto done 20 | } 21 | xs = append(xs, x) 22 | i++ 23 | if i >= size { 24 | oc <- xs 25 | xs = make([]interface{}, 0, size) 26 | i = 0 27 | lastOutput = time.Now() 28 | } 29 | case <-time.After(10 * time.Millisecond): 30 | if time.Since(lastOutput) > maxInterval && i > 0 { 31 | oc <- xs 32 | xs = make([]interface{}, 0, size) 33 | i = 0 34 | lastOutput = time.Now() 35 | } 36 | case <-ctx.Done(): 37 | goto done 38 | } 39 | } 40 | done: 41 | if i > 0 { 42 | oc <- xs 43 | } 44 | }() 45 | return oc 46 | } 47 | -------------------------------------------------------------------------------- /sshauditor/batch_test.go: -------------------------------------------------------------------------------- 1 | package sshauditor 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestBatch(t *testing.T) { 11 | batchInput := make(chan interface{}) 12 | go func() { 13 | for i := 0; i < 10; i++ { 14 | batchInput <- i 15 | } 16 | close(batchInput) 17 | }() 18 | expected := [][]interface{}{ 19 | {0, 1, 2, 3, 4, 5}, 20 | {6, 7, 8, 9}, 21 | } 22 | 23 | i := 0 24 | for o := range batch(context.TODO(), batchInput, 6, 2*time.Second) { 25 | if !reflect.DeepEqual(expected[i], o) { 26 | t.Errorf("Expected %v, got %v", expected[i], o) 27 | } 28 | i += 1 29 | } 30 | } 31 | 32 | func TestBatchWithPause(t *testing.T) { 33 | batchInput := make(chan interface{}) 34 | go func() { 35 | for i := 0; i < 20; i++ { 36 | if i == 3 || i == 11 || i == 17 { 37 | time.Sleep(1 * time.Second) 38 | } 39 | batchInput <- i 40 | } 41 | close(batchInput) 42 | }() 43 | expected := [][]interface{}{ 44 | {0, 1, 2}, 45 | {3, 4, 5, 6, 7, 8}, 46 | {9, 10}, 47 | {11, 12, 13, 14, 15, 16}, 48 | {17, 18, 19}, 49 | } 50 | 51 | i := 0 52 | for o := range batch(context.TODO(), batchInput, 6, 500*time.Millisecond) { 53 | if !reflect.DeepEqual(expected[i], o) { 54 | t.Errorf("Expected %v, got %v", expected[i], o) 55 | } 56 | i += 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /sshauditor/brute.go: -------------------------------------------------------------------------------- 1 | package sshauditor 2 | 3 | import "sync" 4 | 5 | type ScanRequest struct { 6 | hostport string 7 | credentials []Credential 8 | } 9 | 10 | type BruteForceResult struct { 11 | hostport string 12 | cred Credential 13 | err error 14 | result string 15 | } 16 | 17 | func bruteworker(jobs <-chan ScanRequest, results chan<- BruteForceResult) { 18 | for sr := range jobs { 19 | failures := 0 20 | for _, cred := range sr.credentials { 21 | //TODO: make this configurable 22 | //After 5 connection errors, stop trying this host for this run 23 | if failures > 5 { 24 | continue 25 | } 26 | result, err := SSHAuthAttempt(sr.hostport, cred.User, cred.Password) 27 | res := BruteForceResult{ 28 | hostport: sr.hostport, 29 | cred: cred, 30 | result: result, 31 | err: err, 32 | } 33 | results <- res 34 | if err != nil { 35 | failures++ 36 | } 37 | } 38 | } 39 | } 40 | 41 | func bruteForcer(numWorkers int, requests []ScanRequest) chan BruteForceResult { 42 | var wg sync.WaitGroup 43 | 44 | requestChan := make(chan ScanRequest, numWorkers) 45 | go func() { 46 | for _, sr := range requests { 47 | requestChan <- sr 48 | } 49 | close(requestChan) 50 | }() 51 | results := make(chan BruteForceResult, 1000) 52 | 53 | for w := 0; w <= numWorkers; w++ { 54 | wg.Add(1) 55 | go func() { 56 | bruteworker(requestChan, results) 57 | wg.Done() 58 | }() 59 | } 60 | 61 | go func() { 62 | wg.Wait() 63 | close(results) 64 | }() 65 | 66 | return results 67 | } 68 | -------------------------------------------------------------------------------- /sshauditor/logsearch.go: -------------------------------------------------------------------------------- 1 | package sshauditor 2 | 3 | import ( 4 | "fmt" 5 | 6 | splunk "github.com/sebkl/splunk-golang" 7 | ) 8 | 9 | type LogSearcher interface { 10 | GetIPs() ([]string, error) 11 | } 12 | 13 | type SplunkLogSearcher struct { 14 | conn splunk.SplunkConnection 15 | } 16 | 17 | func NewSplunkLogSearcher(baseURL string) LogSearcher { 18 | username, password := promptCredentials() //FIXME: better config? 19 | conn := splunk.SplunkConnection{ 20 | Username: username, 21 | SplunkUser: username, //FIXME: why is this a thing? 22 | Password: password, 23 | SplunkApp: "search", 24 | BaseURL: baseURL, 25 | } 26 | return &SplunkLogSearcher{conn: conn} 27 | } 28 | 29 | func (s *SplunkLogSearcher) GetIPs() ([]string, error) { 30 | var ips []string 31 | _, err := s.conn.Login() 32 | 33 | if err != nil { 34 | return ips, err 35 | } 36 | 37 | //fmt.Println("Session key: ", key.Value) 38 | 39 | rows, _, err := s.conn.Search(`search daysago=2 logcheck user NOT krbtgt | rex "logcheck-(?[0-9.]+)" | table logcheck | dedup logcheck`) 40 | if err != nil { 41 | panic(err) 42 | } 43 | //for _, e := range events { 44 | // fmt.Printf("%v\n", e) 45 | //} 46 | fmt.Printf("\n") 47 | for _, e := range rows { 48 | ip := e.Result["logcheck"].(string) 49 | ips = append(ips, ip) 50 | } 51 | return ips, nil 52 | } 53 | -------------------------------------------------------------------------------- /sshauditor/netutil.go: -------------------------------------------------------------------------------- 1 | package sshauditor 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | ) 7 | 8 | func inc(ip net.IP) { 9 | for j := len(ip) - 1; j >= 0; j-- { 10 | ip[j]++ 11 | if ip[j] > 0 { 12 | break 13 | } 14 | } 15 | } 16 | 17 | func ExpandCIDRs(netblocks []string) ([]string, error) { 18 | var hosts []string 19 | for _, netblock := range netblocks { 20 | //If there's no slash, just treat as a single host 21 | if !strings.ContainsRune(netblock, '/') { 22 | hosts = append(hosts, netblock) 23 | continue 24 | } 25 | ip, ipnet, err := net.ParseCIDR(netblock) 26 | if err != nil { 27 | return hosts, err 28 | } 29 | for h := ip.Mask(ipnet.Mask); ipnet.Contains(h); inc(h) { 30 | hosts = append(hosts, h.String()) 31 | } 32 | } 33 | return hosts, nil 34 | } 35 | 36 | func EnumerateHosts(netblocks []string, exclude []string) ([]string, error) { 37 | var hosts []string 38 | allHosts, err := ExpandCIDRs(netblocks) 39 | if err != nil { 40 | return hosts, err 41 | } 42 | 43 | allExcludeHosts, err := ExpandCIDRs(exclude) 44 | if err != nil { 45 | return hosts, err 46 | } 47 | excludeHosts := make(map[string]bool) 48 | for _, ip := range allExcludeHosts { 49 | excludeHosts[ip] = true 50 | } 51 | 52 | for _, ip := range allHosts { 53 | if _, excluded := excludeHosts[ip]; !excluded { 54 | hosts = append(hosts, ip) 55 | } 56 | } 57 | return hosts, nil 58 | } 59 | -------------------------------------------------------------------------------- /sshauditor/netutil_test.go: -------------------------------------------------------------------------------- 1 | package sshauditor 2 | 3 | import "testing" 4 | 5 | var testCases = []struct { 6 | include []string 7 | exclude []string 8 | expected int 9 | wanterror bool 10 | }{ 11 | {[]string{"192.168.1.0/24"}, []string{}, 256, false}, 12 | {[]string{"192.168.1.0/24"}, []string{"192.168.1.30/32"}, 255, false}, 13 | {[]string{"192.168.1.0/24"}, []string{"192.168.1.30/30"}, 252, false}, 14 | {[]string{"192.168.1.0/33"}, []string{}, 0, true}, 15 | {[]string{"192.168.1.1"}, []string{}, 1, false}, 16 | } 17 | 18 | func TestEnumerateHosts(t *testing.T) { 19 | for _, tt := range testCases { 20 | hosts, err := EnumerateHosts(tt.include, tt.exclude) 21 | if err != nil && tt.wanterror != true { 22 | t.Error(err) 23 | } 24 | if err == nil && tt.wanterror == true { 25 | t.Errorf("EnumerateHosts(%#v, %#v) did not return an error", tt.include, tt.exclude) 26 | } 27 | if len(hosts) != tt.expected { 28 | t.Errorf("EnumerateHosts(%#v, %#v) => len(hosts) is %#v, want %#v", tt.include, tt.exclude, len(hosts), tt.expected) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /sshauditor/prompt.go: -------------------------------------------------------------------------------- 1 | package sshauditor 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "syscall" 9 | 10 | "golang.org/x/crypto/ssh/terminal" 11 | ) 12 | 13 | func promptCredentials() (string, string) { 14 | reader := bufio.NewReader(os.Stdin) 15 | 16 | fmt.Fprintf(os.Stderr, "Enter Username: ") 17 | username, _ := reader.ReadString('\n') 18 | 19 | fmt.Fprintf(os.Stderr, "Enter Password: ") 20 | bytePassword, err := terminal.ReadPassword(int(syscall.Stdin)) 21 | fmt.Fprintf(os.Stderr, "\n") 22 | if err != nil { 23 | panic(err) 24 | } 25 | password := string(bytePassword) 26 | 27 | return strings.TrimSpace(username), strings.TrimSpace(password) 28 | } 29 | -------------------------------------------------------------------------------- /sshauditor/sshutil.go: -------------------------------------------------------------------------------- 1 | package sshauditor 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strings" 7 | "time" 8 | 9 | log "github.com/inconshreveable/log15" 10 | "golang.org/x/crypto/ssh" 11 | ) 12 | 13 | var falsePositiveBanners = [...]string{ 14 | "Auth User/Pass with PS...fail...Please reconnect", 15 | } 16 | 17 | //isFalsePositiveBanner returns true if the ssh login banner 18 | //appears to be a false positive. This could probably just check 19 | //for the presense of 'uid=' but for now, check for known banners 20 | func isFalsePositiveBanner(output string) bool { 21 | for _, b := range falsePositiveBanners { 22 | if strings.Contains(output, b) { 23 | return true 24 | } 25 | } 26 | return false 27 | } 28 | 29 | //isPrivateKey returns true if the passed string is a ssh private key instead 30 | //of a password 31 | func isPrivateKey(s string) bool { 32 | return strings.HasPrefix(s, "-----BEGIN") 33 | } 34 | 35 | //DialWithDeadline is identical to ssh.Dial except that it calls SetDeadline on 36 | //the underlying connection 37 | func DialWithDeadline(network, addr string, config *ssh.ClientConfig) (*ssh.Client, error) { 38 | conn, err := net.DialTimeout(network, addr, config.Timeout) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | //This call to SetDeadline is the only difference from ssh.Dial 44 | conn.SetDeadline(time.Now().Add(2 * config.Timeout)) 45 | c, chans, reqs, err := ssh.NewClientConn(conn, addr, config) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return ssh.NewClient(c, chans, reqs), nil 50 | } 51 | 52 | func FetchSSHKeyFingerprint(hostport string) string { 53 | 54 | var keyFingerprint string 55 | 56 | DumpHostkey := func(hostname string, remote net.Addr, key ssh.PublicKey) error { 57 | fp := ssh.FingerprintSHA256(key) 58 | keyFingerprint = fp 59 | return nil 60 | } 61 | 62 | user := "security" 63 | host, _, err := net.SplitHostPort(hostport) 64 | if err == nil { 65 | user = fmt.Sprintf("logcheck-%s", host) 66 | } 67 | 68 | config := &ssh.ClientConfig{ 69 | User: user, 70 | Auth: []ssh.AuthMethod{ 71 | ssh.Password("security"), 72 | }, 73 | HostKeyCallback: DumpHostkey, 74 | Timeout: 4 * time.Second, 75 | ClientVersion: "SSH-2.0-Go-ssh-auditor", 76 | } 77 | 78 | client, err := DialWithDeadline("tcp", hostport, config) 79 | if err == nil { 80 | //This was supposed to fail 81 | client.Close() 82 | log.Error("initial probe worked!?!?", "host", hostport, "user", user, "password", "security") 83 | } 84 | return keyFingerprint 85 | } 86 | 87 | func SSHExecAttempt(client *ssh.Client, hostport string) bool { 88 | session, err := client.NewSession() 89 | if err != nil { 90 | log.Error("successful login but failed to open session", "host", hostport) 91 | return false 92 | } 93 | defer session.Close() 94 | out, err := session.CombinedOutput("id") 95 | if err != nil { 96 | log.Error("successful login but failed to run id", "host", hostport) 97 | return false 98 | } 99 | if isFalsePositiveBanner(string(out)) { 100 | log.Error("successful login but unexpected id command output", "host", hostport, "output", string(out)) 101 | return false 102 | } 103 | return true 104 | } 105 | 106 | func SSHDialAttempt(client *ssh.Client, dest string) bool { 107 | //If there was no error, the dial worked and this is vulnerable! 108 | conn, err := client.Dial("tcp", dest) 109 | if err == nil { 110 | conn.Close() 111 | return true 112 | } 113 | //It may only allow local forwarding, so try replacing the ip with localhost 114 | log.Error("tunnel attempt failed", "error", err) 115 | _, port, err := net.SplitHostPort(dest) 116 | if err != nil { 117 | log.Error("Invalid host port in SSHDialAttempt, should not happen", "error", err) 118 | return false 119 | } 120 | newDest := fmt.Sprintf("127.0.0.1:%s", port) 121 | conn, err = client.Dial("tcp", newDest) 122 | if err == nil { 123 | conn.Close() 124 | return true 125 | } 126 | log.Error("tunnel attempt failed", "error", err) 127 | return false 128 | } 129 | 130 | func challengeReponder(password string) ssh.KeyboardInteractiveChallenge { 131 | return func(user, instruction string, questions []string, echos []bool) ([]string, error) { 132 | answers := make([]string, len(questions)) 133 | for i := range answers { 134 | answers[i] = password 135 | } 136 | return answers, nil 137 | } 138 | } 139 | 140 | func genAuthMethod(password string) ([]ssh.AuthMethod, error) { 141 | if isPrivateKey(password) { 142 | signer, err := ssh.ParsePrivateKey([]byte(password)) 143 | if err != nil { 144 | return []ssh.AuthMethod{}, err 145 | } 146 | 147 | return []ssh.AuthMethod{ 148 | ssh.PublicKeys(signer), 149 | }, nil 150 | 151 | } 152 | return []ssh.AuthMethod{ 153 | ssh.Password(password), 154 | ssh.KeyboardInteractive(challengeReponder(password)), 155 | }, nil 156 | } 157 | 158 | func SSHAuthAttempt(hostport, user, password string) (string, error) { 159 | authMethods, err := genAuthMethod(password) 160 | if err != nil { 161 | return "", err 162 | } 163 | config := &ssh.ClientConfig{ 164 | User: user, 165 | Auth: authMethods, 166 | HostKeyCallback: ssh.InsecureIgnoreHostKey(), 167 | Timeout: 4 * time.Second, 168 | ClientVersion: "SSH-2.0-Go-ssh-auditor", 169 | } 170 | client, err := DialWithDeadline("tcp", hostport, config) 171 | if err != nil { 172 | //FIXME: better way? 173 | if strings.Contains(err.Error(), "unable to authenticate") { 174 | return "", nil 175 | } 176 | return "", err 177 | } 178 | //Found a potential weak password! 179 | defer client.Close() 180 | 181 | execSuccess := SSHExecAttempt(client, hostport) 182 | if execSuccess { 183 | return "exec", nil 184 | } 185 | //If I was able to authenticate but was unable to run a command, see if port forwarding works 186 | 187 | tcpSuccess := SSHDialAttempt(client, hostport) 188 | if tcpSuccess { 189 | return "tunnel", nil 190 | } 191 | return "auth", nil 192 | } 193 | -------------------------------------------------------------------------------- /sshauditor/sshutil_test.go: -------------------------------------------------------------------------------- 1 | package sshauditor 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "testing" 8 | ) 9 | 10 | type authTestCase struct { 11 | hostport string 12 | user string 13 | password string 14 | expected string 15 | wanterr bool 16 | } 17 | 18 | var authTestCases = []authTestCase{ 19 | { 20 | "alpine-sshd-ok:22", 21 | "root", 22 | "test", 23 | "", 24 | false, 25 | }, 26 | { 27 | "alpine-sshd-test-test:22", 28 | "test", 29 | "test", 30 | "exec", 31 | false, 32 | }, 33 | { 34 | "alpine-sshd-test-blank:22", 35 | "test", 36 | "", 37 | "exec", 38 | false, 39 | }, 40 | { 41 | "alpine-sshd-test-test-no-id-binary:22", 42 | "test", 43 | "test", 44 | "tunnel", 45 | false, 46 | }, 47 | { 48 | "alpine-sshd-test-test-no-id-binary-tunnel-local:22", 49 | "test", 50 | "test", 51 | "tunnel", 52 | false, 53 | }, 54 | { 55 | "alpine-sshd-test-test-no-id-binary-no-tunnel:22", 56 | "test", 57 | "test", 58 | "auth", 59 | false, 60 | }, 61 | } 62 | 63 | func init() { 64 | key, err := ioutil.ReadFile("../testing/docker/alpine-sshd-test-key/test.key") 65 | if err != nil { 66 | log.Printf("Can't read test key: %v", err) 67 | return 68 | } 69 | //log.Printf("Using key %v", key) 70 | authTestCases = append(authTestCases, authTestCase{ 71 | hostport: "alpine-sshd-test-key:22", 72 | user: "test", 73 | password: string(key), 74 | expected: "exec", 75 | wanterr: false, 76 | }) 77 | } 78 | 79 | func TestSSHAuthAttempt(t *testing.T) { 80 | if testing.Short() { 81 | t.Skip("skipping test in short mode.") 82 | } 83 | for _, tt := range authTestCases { 84 | t.Run(fmt.Sprintf("SSHAuthAttempt(%q, %q, %q) => %q", tt.hostport, tt.user, tt.password, tt.expected), func(t *testing.T) { 85 | resp, err := SSHAuthAttempt(tt.hostport, tt.user, tt.password) 86 | if err != nil && tt.wanterr != true { 87 | t.Errorf("Unexpected error %v", err) 88 | } 89 | if err == nil && tt.wanterr == true { 90 | t.Errorf("did not return an expected error") 91 | } 92 | if resp != tt.expected { 93 | t.Errorf("got %#v, want %#v", resp, tt.expected) 94 | } 95 | }) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /sshauditor/sshworker.go: -------------------------------------------------------------------------------- 1 | package sshauditor 2 | 3 | import "sync" 4 | 5 | type SSHHost struct { 6 | hostport string 7 | version string 8 | keyfp string 9 | } 10 | 11 | func keyworker(jobs <-chan ScanResult, results chan<- SSHHost) { 12 | for host := range jobs { 13 | if !host.success { 14 | continue 15 | } 16 | res := SSHHost{ 17 | hostport: host.hostport, 18 | version: host.banner, 19 | keyfp: FetchSSHKeyFingerprint(host.hostport), 20 | } 21 | results <- res 22 | } 23 | } 24 | 25 | func fingerPrintFetcher(numWorkers int, scanResults <-chan ScanResult) chan SSHHost { 26 | var wg sync.WaitGroup 27 | 28 | results := make(chan SSHHost, 1024) 29 | 30 | for w := 0; w <= numWorkers; w++ { 31 | wg.Add(1) 32 | go func() { 33 | keyworker(scanResults, results) 34 | wg.Done() 35 | }() 36 | } 37 | 38 | go func() { 39 | wg.Wait() 40 | close(results) 41 | }() 42 | 43 | return results 44 | } 45 | -------------------------------------------------------------------------------- /sshauditor/store.go: -------------------------------------------------------------------------------- 1 | package sshauditor 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | 7 | "github.com/jmoiron/sqlx" 8 | _ "github.com/mattn/go-sqlite3" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | const schema = ` 13 | CREATE TABLE IF NOT EXISTS hosts ( 14 | hostport character varying, 15 | version character varying, 16 | fingerprint character varying, 17 | seen_first REAL, 18 | seen_last REAL, 19 | 20 | PRIMARY KEY (hostport) 21 | ); 22 | 23 | CREATE TABLE IF NOT EXISTS credentials ( 24 | user character varying, 25 | password character varying, 26 | scan_interval DEFAULT 14, 27 | 28 | PRIMARY KEY (user, password) 29 | ); 30 | 31 | CREATE TABLE IF NOT EXISTS host_creds ( 32 | hostport character varying, 33 | user character varying, 34 | password character varying, 35 | last_tested REAL, 36 | result character varying, 37 | scan_interval DEFAULT 0, 38 | 39 | PRIMARY KEY (hostport, user, password) 40 | ); 41 | 42 | CREATE TABLE IF NOT EXISTS host_changes ( 43 | time REAL, 44 | hostport character varying, 45 | type character varying, 46 | old character varying, 47 | new character varying 48 | ); 49 | 50 | -- Migrate 51 | PRAGMA writable_schema=1; 52 | UPDATE sqlite_master SET SQL=REPLACE(SQL, 'priority', 'scan_interval') WHERE name='host_creds'; 53 | UPDATE sqlite_master SET SQL=REPLACE(SQL, 'priority', 'scan_interval') WHERE name='credentials'; 54 | PRAGMA writable_schema=0; 55 | -- UPDATE credentials set scan_interval=14 where scan_interval == 0; 56 | -- UPDATE host_creds set scan_interval=14 where scan_interval == 0; 57 | CREATE INDEX IF NOT EXISTS host_creds_vulnerable ON host_creds (result) WHERE result != ''; 58 | ` 59 | 60 | type Host struct { 61 | Hostport string 62 | Version string 63 | Fingerprint string 64 | SeenFirst string `db:"seen_first"` 65 | SeenLast string `db:"seen_last"` 66 | } 67 | 68 | type Credential struct { 69 | User string 70 | Password string 71 | ScanInterval int `db:"scan_interval"` 72 | } 73 | 74 | func (c Credential) String() string { 75 | return fmt.Sprintf("%s:%s every %d days", c.User, c.Password, c.ScanInterval) 76 | } 77 | 78 | type HostCredential struct { 79 | Hostport string `json:"-"` 80 | User string 81 | Password string 82 | LastTested string `db:"last_tested"` 83 | Result string 84 | ScanInterval int `db:"scan_interval"` 85 | } 86 | 87 | type Vulnerability struct { 88 | HostCredential 89 | Host `db:"host"` 90 | } 91 | 92 | type SQLiteStore struct { 93 | conn *sqlx.DB 94 | tx *sqlx.Tx 95 | txDepth int 96 | } 97 | 98 | func NewSQLiteStore(uri string) (*SQLiteStore, error) { 99 | conn, err := sqlx.Open("sqlite3", uri) 100 | if err != nil { 101 | return nil, err 102 | } 103 | return &SQLiteStore{conn: conn}, nil 104 | } 105 | 106 | func (s *SQLiteStore) Close() error { 107 | return s.conn.Close() 108 | } 109 | 110 | func (s *SQLiteStore) Init() error { 111 | _, err := s.conn.Exec(schema) 112 | return errors.Wrap(err, "Init() failed") 113 | } 114 | 115 | func (s *SQLiteStore) Begin() (*sqlx.Tx, error) { 116 | if s.tx != nil { 117 | s.txDepth += 1 118 | //log.Printf("Returning existing transaction: depth=%d\n", s.txDepth) 119 | return s.tx, nil 120 | } 121 | //log.Printf("new transaction\n") 122 | tx, err := s.conn.Beginx() 123 | if err != nil { 124 | return tx, err 125 | } 126 | s.tx = tx 127 | s.txDepth += 1 128 | return s.tx, nil 129 | } 130 | 131 | func (s *SQLiteStore) Commit() error { 132 | if s.tx == nil { 133 | return errors.New("Commit outside of transaction") 134 | } 135 | s.txDepth -= 1 136 | if s.txDepth > 0 { 137 | //log.Printf("Not commiting stacked transaction: depth=%d\n", s.txDepth) 138 | return nil // No OP 139 | } 140 | //log.Printf("Commiting transaction: depth=%d\n", s.txDepth) 141 | err := s.tx.Commit() 142 | s.tx = nil 143 | return err 144 | } 145 | 146 | func (s *SQLiteStore) Exec(query string, args ...interface{}) (sql.Result, error) { 147 | tx, err := s.Begin() 148 | defer s.Commit() 149 | if err != nil { 150 | return nil, err 151 | } 152 | return tx.Exec(query, args...) 153 | } 154 | func (s *SQLiteStore) Select(dest interface{}, query string, args ...interface{}) error { 155 | tx, err := s.Begin() 156 | defer s.Commit() 157 | if err != nil { 158 | return err 159 | } 160 | return tx.Select(dest, query, args...) 161 | } 162 | func (s *SQLiteStore) Get(dest interface{}, query string, args ...interface{}) error { 163 | tx, err := s.Begin() 164 | defer s.Commit() 165 | if err != nil { 166 | return err 167 | } 168 | return tx.Get(dest, query, args...) 169 | } 170 | 171 | func (s *SQLiteStore) AddCredential(c Credential) (bool, error) { 172 | res, err := s.Exec( 173 | "INSERT OR IGNORE INTO credentials (user, password, scan_interval) VALUES ($1, $2, $3)", 174 | c.User, c.Password, c.ScanInterval) 175 | if err != nil { 176 | return false, errors.Wrap(err, "AddCredential") 177 | } 178 | affected, err := res.RowsAffected() 179 | if err != nil { 180 | return false, errors.Wrap(err, "AddCredential") 181 | } 182 | added := affected == 1 183 | _, err = s.Exec( 184 | "UPDATE credentials SET scan_interval=$1 WHERE user=$2 AND password=$3", 185 | c.ScanInterval, c.User, c.Password) 186 | 187 | return added, errors.Wrap(err, "AddCredential") 188 | } 189 | 190 | func (s *SQLiteStore) getKnownHosts() (map[string]Host, error) { 191 | hostList := []Host{} 192 | 193 | hosts := make(map[string]Host) 194 | 195 | err := s.Select(&hostList, "SELECT * FROM hosts") 196 | if err != nil { 197 | return hosts, errors.Wrap(err, "getKnownHosts") 198 | } 199 | for _, h := range hostList { 200 | hosts[h.Hostport] = h 201 | } 202 | return hosts, nil 203 | } 204 | 205 | func (s *SQLiteStore) resetHostCreds(h SSHHost) error { 206 | _, err := s.Exec("UPDATE host_creds set last_tested=0 where hostport=$1", h.hostport) 207 | return err 208 | } 209 | 210 | func (s *SQLiteStore) ResetInterval() error { 211 | _, err := s.Exec("UPDATE host_creds set last_tested=0") 212 | return err 213 | } 214 | 215 | func (s *SQLiteStore) ResetCreds() error { 216 | _, err := s.Exec("DELETE from host_creds") 217 | if err != nil { 218 | return err 219 | } 220 | _, err = s.Exec("DELETE from credentials") 221 | return err 222 | } 223 | 224 | func (s *SQLiteStore) addOrUpdateHost(h SSHHost) error { 225 | err := s.resetHostCreds(h) 226 | if err != nil { 227 | return errors.Wrap(err, "addOrUpdateHost") 228 | } 229 | res, err := s.Exec( 230 | `UPDATE hosts SET version=$1,fingerprint=$2,seen_last=datetime('now', 'localtime') 231 | WHERE hostport=$3`, 232 | h.version, h.keyfp, h.hostport) 233 | if err != nil { 234 | return err 235 | } 236 | rows, err := res.RowsAffected() 237 | if rows != 0 { 238 | return errors.Wrap(err, "addOrUpdateHost") 239 | } 240 | _, err = s.Exec( 241 | `INSERT INTO hosts (hostport, version, fingerprint, seen_first, seen_last) VALUES 242 | ($1, $2, $3, datetime('now', 'localtime'), datetime('now', 'localtime'))`, 243 | h.hostport, h.version, h.keyfp) 244 | return err 245 | } 246 | 247 | func (s *SQLiteStore) setLastSeen(h SSHHost) error { 248 | _, err := s.Exec( 249 | "UPDATE hosts SET seen_last=datetime('now', 'localtime') WHERE hostport=$1", 250 | h.hostport) 251 | return errors.Wrap(err, "setLastSeen") 252 | } 253 | 254 | func (s *SQLiteStore) addHostChange(h SSHHost, changeType, old, new string) error { 255 | q := `INSERT INTO host_changes (time, hostport, type, old, new) VALUES 256 | (datetime('now', 'localtime'), $1, $2, $3, $4)` 257 | _, err := s.Exec(q, h.hostport, changeType, old, new) 258 | return errors.Wrap(err, "addHostChanges failed") 259 | } 260 | 261 | func (s *SQLiteStore) addHostChanges(new SSHHost, old Host) error { 262 | var err error 263 | if old.Fingerprint != new.keyfp { 264 | err = s.addHostChange(new, "fingerprint", old.Fingerprint, new.keyfp) 265 | if err != nil { 266 | return errors.Wrap(err, "addHostChange") 267 | } 268 | } 269 | if old.Version != new.version { 270 | err = s.addHostChange(new, "version", old.Version, new.version) 271 | } 272 | return errors.Wrap(err, "addHostChange") 273 | } 274 | 275 | func (s *SQLiteStore) GetAllCreds() ([]Credential, error) { 276 | credentials := []Credential{} 277 | err := s.Select(&credentials, "SELECT user, password, scan_interval from credentials") 278 | return credentials, errors.Wrap(err, "getAllCreds") 279 | } 280 | 281 | func (s *SQLiteStore) initHostCreds() (int, error) { 282 | _, err := s.Begin() 283 | defer s.Commit() 284 | if err != nil { 285 | return 0, errors.Wrap(err, "initHostCreds") 286 | } 287 | creds, err := s.GetAllCreds() 288 | if err != nil { 289 | return 0, errors.Wrap(err, "initHostCreds") 290 | } 291 | 292 | knownHosts, err := s.GetActiveHosts(7) 293 | if err != nil { 294 | return 0, errors.Wrap(err, "initHostCreds") 295 | } 296 | 297 | inserted := 0 298 | for _, host := range knownHosts { 299 | ins, err := s.initHostCredsForHost(creds, host) 300 | if err != nil { 301 | return inserted, errors.Wrap(err, "initHostCreds") 302 | } 303 | inserted += ins 304 | } 305 | return inserted, nil 306 | } 307 | func (s *SQLiteStore) initHostCredsForHost(creds []Credential, h Host) (int, error) { 308 | inserted := 0 309 | for _, c := range creds { 310 | res, err := s.Exec(`INSERT OR IGNORE INTO host_creds (hostport, user, password, last_tested, result, scan_interval) VALUES 311 | ($1, $2, $3, 0, '', $4)`, 312 | h.Hostport, c.User, c.Password, c.ScanInterval) 313 | if err != nil { 314 | return inserted, errors.Wrap(err, "initHostCredsForHost") 315 | } 316 | rows, err := res.RowsAffected() 317 | inserted += int(rows) 318 | } 319 | return inserted, nil 320 | } 321 | 322 | func (s *SQLiteStore) getScanQueueHelper(query string) ([]ScanRequest, error) { 323 | requestMap := make(map[string]*ScanRequest) 324 | var requests []ScanRequest 325 | credentials := []HostCredential{} 326 | err := s.Select(&credentials, query) 327 | if err != nil { 328 | return requests, errors.Wrap(err, "getScanQueueHelper") 329 | } 330 | 331 | for _, hc := range credentials { 332 | sr := requestMap[hc.Hostport] 333 | if sr == nil { 334 | sr = &ScanRequest{ 335 | hostport: hc.Hostport, 336 | } 337 | } 338 | sr.credentials = append(sr.credentials, Credential{User: hc.User, Password: hc.Password}) 339 | requestMap[hc.Hostport] = sr 340 | } 341 | 342 | for _, sr := range requestMap { 343 | requests = append(requests, *sr) 344 | } 345 | 346 | return requests, nil 347 | } 348 | func (s *SQLiteStore) getScanQueue() ([]ScanRequest, error) { 349 | q := `select host_creds.* from host_creds, hosts 350 | where hosts.hostport = host_creds.hostport and 351 | last_tested < datetime('now', 'localtime', -scan_interval || ' day') and 352 | hosts.fingerprint != '' and 353 | seen_last > datetime('now', 'localtime', '-7 day') order by last_tested ASC` 354 | return s.getScanQueueHelper(q) 355 | } 356 | func (s *SQLiteStore) getScanQueueSize() (int, error) { 357 | q := `select count(*) from host_creds, hosts 358 | where hosts.hostport = host_creds.hostport and 359 | last_tested < datetime('now', 'localtime', -scan_interval || ' day') and 360 | hosts.fingerprint != '' and 361 | seen_last > datetime('now', 'localtime', '-7 day')` 362 | 363 | var cnt int 364 | err := s.Get(&cnt, q) 365 | return cnt, errors.Wrap(err, "getScanQueueSize") 366 | } 367 | func (s *SQLiteStore) getRescanQueue() ([]ScanRequest, error) { 368 | q := `select * from host_creds where result !='' order by last_tested ASC` 369 | return s.getScanQueueHelper(q) 370 | } 371 | 372 | func (s *SQLiteStore) updateBruteResult(br BruteForceResult) error { 373 | if br.err != nil { 374 | //If this BruteForceResult was an error.. as in, not a positive or 375 | //negative result, don't update anything. We can't say definitively 376 | //that the credential does or does not work. 377 | return nil 378 | } 379 | _, err := s.Exec(`UPDATE host_creds set last_tested=datetime('now', 'localtime'), result=$1 380 | WHERE hostport=$2 AND user=$3 AND password=$4`, 381 | br.result, br.hostport, br.cred.User, br.cred.Password) 382 | if err != nil { 383 | return errors.Wrap(err, "updateBruteResult") 384 | } 385 | //Also update the seen_last field on the hosts table, since a non-err 386 | //BruteForceResult means the system was reachable. 387 | _, err = s.Exec( 388 | "UPDATE hosts SET seen_last=datetime('now', 'localtime') WHERE hostport=$1", 389 | br.hostport) 390 | return errors.Wrap(err, "updateBruteResult") 391 | } 392 | 393 | func (s *SQLiteStore) GetVulnerabilities() ([]Vulnerability, error) { 394 | creds := []Vulnerability{} 395 | q := `select 396 | hc.hostport, hc.user, hc.password, hc.result, hc.last_tested, 397 | h.version "host.version", h.hostport "host.hostport", 398 | h.seen_first "host.seen_first", h.seen_last "host.seen_last", h.fingerprint "host.fingerprint" 399 | from 400 | host_creds hc, hosts h 401 | where 402 | h.hostport = hc.hostport 403 | and result!='' order by last_tested asc` 404 | 405 | err := s.Select(&creds, q) 406 | return creds, errors.Wrap(err, "GetVulnerabilities") 407 | } 408 | 409 | //GetActiveHosts returns a list of hosts seen at most maxAgeDays ago 410 | func (s *SQLiteStore) GetActiveHosts(maxAgeDays int) ([]Host, error) { 411 | hostList := []Host{} 412 | dayInterval := fmt.Sprintf("-%d day", maxAgeDays) 413 | query := `SELECT * FROM hosts WHERE seen_last >= datetime('now', 'localtime', $1)` 414 | err := s.Select(&hostList, query, dayInterval) 415 | return hostList, errors.Wrap(err, "GetActiveHosts") 416 | } 417 | 418 | func (s *SQLiteStore) DeleteHost(hostport string) error { 419 | s.Begin() 420 | defer s.Commit() 421 | _, err := s.Exec("DELETE FROM hosts where hostport=$1", hostport) 422 | if err != nil { 423 | return err 424 | } 425 | _, err = s.Exec("DELETE FROM host_creds where hostport=$1", hostport) 426 | return err 427 | } 428 | -------------------------------------------------------------------------------- /sshauditor/store_test.go: -------------------------------------------------------------------------------- 1 | package sshauditor 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAddCredential(t *testing.T) { 8 | check := func(e error) { 9 | if e != nil { 10 | t.Fatal(e) 11 | } 12 | } 13 | s, err := NewSQLiteStore(":memory:") 14 | check(err) 15 | err = s.Init() 16 | check(err) 17 | cred := Credential{User: "foo", Password: "foo", ScanInterval: 5} 18 | 19 | added, err := s.AddCredential(cred) 20 | check(err) 21 | if added != true { 22 | t.Errorf("Expected added to be true") 23 | } 24 | 25 | creds, err := s.GetAllCreds() 26 | check(err) 27 | if len(creds) != 1 { 28 | t.Errorf("Expected 1 cred, got %d", len(creds)) 29 | } 30 | if creds[0] != cred { 31 | t.Errorf("Expected %v, got %v", cred, creds[0]) 32 | } 33 | 34 | cred = Credential{User: "foo", Password: "foo", ScanInterval: 10} 35 | added, err = s.AddCredential(cred) 36 | check(err) 37 | if added != false { 38 | t.Errorf("Expected added to be false") 39 | } 40 | creds, err = s.GetAllCreds() 41 | check(err) 42 | if len(creds) != 1 { 43 | t.Errorf("Expected 1 cred, got %d", len(creds)) 44 | } 45 | if creds[0] != cred { 46 | t.Errorf("Expected %q, got %q", cred, creds[0]) 47 | } 48 | 49 | } 50 | 51 | func TestAddAndDeleteHost(t *testing.T) { 52 | check := func(e error) { 53 | if e != nil { 54 | t.Fatal(e) 55 | } 56 | } 57 | s, err := NewSQLiteStore(":memory:") 58 | check(err) 59 | err = s.Init() 60 | check(err) 61 | 62 | s.addOrUpdateHost(SSHHost{ 63 | hostport: "192.168.1.1:22", 64 | version: "whatever", 65 | keyfp: "whatever", 66 | }) 67 | knownHosts, err := s.GetActiveHosts(7) 68 | check(err) 69 | if knownHosts[0].Hostport != "192.168.1.1:22" { 70 | knownHosts, err := s.GetActiveHosts(7) 71 | check(err) 72 | if len(knownHosts) != 1 { 73 | t.Fatalf("Expected 1 host, got %d", len(knownHosts)) 74 | } 75 | t.Fatalf("Expected 192.168.1.1:22 , got %s", knownHosts[0].Hostport) 76 | } 77 | 78 | err = s.DeleteHost("192.168.1.1:22") 79 | check(err) 80 | knownHosts, err = s.GetActiveHosts(7) 81 | check(err) 82 | if len(knownHosts) != 0 { 83 | t.Fatalf("Expected 0 hosts, got %d", len(knownHosts)) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /testing/docker/alpine-sshd-ok/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | RUN apk update 3 | RUN apk add openssh 4 | RUN ssh-keygen -A 5 | EXPOSE 22 6 | CMD ["/usr/sbin/sshd", "-D"] 7 | -------------------------------------------------------------------------------- /testing/docker/alpine-sshd-test-blank/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | RUN apk update 3 | RUN apk add openssh 4 | RUN ssh-keygen -A 5 | RUN echo -e "\n\n"|adduser test 6 | RUN echo 'PermitEmptyPasswords yes' >> /etc/ssh/sshd_config 7 | EXPOSE 22 8 | CMD ["/usr/sbin/sshd", "-D"] 9 | -------------------------------------------------------------------------------- /testing/docker/alpine-sshd-test-key/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | RUN apk update 3 | RUN apk add openssh 4 | RUN ssh-keygen -A 5 | RUN echo LogLevel DEBUG >> /etc/ssh/sshd_config 6 | RUN echo -e "XXX\nXXX"|adduser test 7 | RUN mkdir /home/test/.ssh 8 | ADD test.pub /home/test/.ssh/authorized_keys 9 | RUN chmod 0700 /home/test /home/test/.ssh && \ 10 | chmod 0600 /home/test/.ssh/authorized_keys && \ 11 | chown test:test /home/test/.ssh /home/test/.ssh/authorized_keys 12 | EXPOSE 22 13 | CMD ["/usr/sbin/sshd", "-D"] 14 | -------------------------------------------------------------------------------- /testing/docker/alpine-sshd-test-key/test.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEAp4zHobusIqkoUwbqRbK/ZdpxyOLMwb5EhbpLssSQXtb9AnTd 3 | mQXhPJrFYUWX3bS0BrIOtNCx/zr0iTRatx7zunFHLWMvUkNE3SE87lcGdTok/IKu 4 | DawMITREf/rG3Q0VIp3f+pmyjiFP9vqUgZJjF33gUSEIgRkURTjdYV+ni1saICc2 5 | CAI3D5hWGUUpLXnl0sEjbXrVZe5rv1rSf6tkoP9GRNd2eBWlmaH8LcPJ7mIZoKJ1 6 | ERBFU/TBXS7q7UCk4uGOesK+De6+fmhhr7q5KRpjMqEo70S/NR3iN7aa6ZTbwsuD 7 | /HznPAxH5Gt9sWun5ObEtngeDuJ/AvdVtrjAzQIDAQABAoIBADm0T5DRHOC3F484 8 | w3iLrkV67VM9YT2ctObFmYqRDhPLwUmWTDeGgeMofrAYkswljxb7PftiINuBR3T1 9 | lgSZYWGl39iu3z5asp3h86U2TaAUDWb17tIAnhDVen4FNXYuKkhIzTn2szsmF+88 10 | r+bMLtqJXLc0RESKCaQpiH0fv4lLoVEBmGwgEoOAXE/+l1QxJ7Qyg6WKBA4pVKRv 11 | G4vSrtnzK8gykwVHFrZ2d37WNw8LDDF9/1xPydQeIlRtx3yox/NQ4gQPGjiG/mZa 12 | W8f/yrzcjJR7MWPW9+X9hKFESVJOXwZu2SY4pbN7TZAP4Up/JGf1mqZ4c+LqX+H0 13 | 2xllPDUCgYEA0gsp7kIHC2ehmRg4rWAF11HoFYpPTBGabiM4WgtGc5fmlO2nfvKf 14 | G/eo926YPX9Dh6xNuQPMO0dKLtLbvPZd8iPEbF+SinJZRgNyUYzOnoisD8xiXmFi 15 | vl2LN9qIGPm+JkpcZviZX59dP4fDtapiGnZ//Eo1JTvVHNssvJYXfHMCgYEAzDV8 16 | NCS/ybkOwFt32sKX9ImZ9eYCZ1Xgp/3tmCrrFkC0uj1nKnW6/3slplUmA844hJZi 17 | FfTMpuM2NbvaqRlxCIN32qExSYk4AIvUV38DQYicClKXmbcbXNlH5F6OFG9CEYBl 18 | xbQrE2xa/9cHyYsE5FrnjYu5Hg5VebKY/Nfmvb8CgYAq6hkBEwB5dgzwSKCb27iV 19 | y6hNblrRQe95fTywOQmF6I019HB6YXvcrPcaxr/YIL5dY/jlSj9aBTDE7FP1Aakk 20 | Jeal9cjs/Hv88AtVWh2FCouNFscqXV9dYaNmIAIaiDwvepXye+xqOxdEOark3RD6 21 | MyfnFgpRty3NnYJAbbISvQKBgB5jw0UbRvNlN93GfvKuTODWbXQbs7XfINy/PFGN 22 | 6YhZPswMgULfXGHS2MqFF3Avi1en7Zbo5F/dpl6ewy6xG5Ixxm9h16zi39Os+3+9 23 | 5iUliFs86VS13DuYFkvVCn3eBM4H9p1Sb6qnUG+Md5s1theps5Xr+jOP3IaM3pma 24 | I7uPAoGAeKAiWu6YxZPy+5jcCFAp11vZYVe4xa0rkiT5u0twYvZ7p3aPZyG9HAs6 25 | +iHgUBOPXgAVTOoVxrUG+amyGv9pevykxL5FiTyWgwYFhzgub1oSMn31cQEaQfCr 26 | qZ8L6QMy1XjeKOWEAHM0Xj6h9xk9i3THDdCjLSF/fpys0+zu92c= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /testing/docker/alpine-sshd-test-key/test.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCnjMehu6wiqShTBupFsr9l2nHI4szBvkSFukuyxJBe1v0CdN2ZBeE8msVhRZfdtLQGsg600LH/OvSJNFq3HvO6cUctYy9SQ0TdITzuVwZ1OiT8gq4NrAwhNER/+sbdDRUind/6mbKOIU/2+pSBkmMXfeBRIQiBGRRFON1hX6eLWxogJzYIAjcPmFYZRSkteeXSwSNtetVl7mu/WtJ/q2Sg/0ZE13Z4FaWZofwtw8nuYhmgonUREEVT9MFdLurtQKTi4Y56wr4N7r5+aGGvurkpGmMyoSjvRL81HeI3tprplNvCy4P8fOc8DEfka32xa6fk5sS2eB4O4n8C91W2uMDN justinazoff@penguin 2 | -------------------------------------------------------------------------------- /testing/docker/alpine-sshd-test-test-no-id-binary-no-tunnel/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | RUN apk update 3 | RUN apk add openssh 4 | RUN ssh-keygen -A 5 | RUN echo -e "test\ntest"|adduser test 6 | RUN rm /usr/bin/id 7 | RUN echo AllowTcpForwarding no >> /etc/ssh/sshd_config 8 | EXPOSE 22 9 | CMD ["/usr/sbin/sshd", "-D"] 10 | -------------------------------------------------------------------------------- /testing/docker/alpine-sshd-test-test-no-id-binary-tunnel-local/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | RUN apk update 3 | RUN apk add openssh 4 | RUN ssh-keygen -A 5 | RUN echo -e "test\ntest"|adduser test 6 | RUN rm /usr/bin/id 7 | ADD sshd_config /etc/ssh/sshd_config 8 | EXPOSE 22 9 | CMD ["/usr/sbin/sshd", "-D"] 10 | -------------------------------------------------------------------------------- /testing/docker/alpine-sshd-test-test-no-id-binary-tunnel-local/sshd_config: -------------------------------------------------------------------------------- 1 | AuthorizedKeysFile .ssh/authorized_keys 2 | AllowTcpForwarding local 3 | DisableForwarding no 4 | GatewayPorts no 5 | X11Forwarding no 6 | Subsystem sftp /usr/lib/ssh/sftp-server 7 | -------------------------------------------------------------------------------- /testing/docker/alpine-sshd-test-test-no-id-binary/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | RUN apk update 3 | RUN apk add openssh 4 | RUN ssh-keygen -A 5 | RUN echo -e "test\ntest"|adduser test 6 | RUN rm /usr/bin/id 7 | ADD sshd_config /etc/ssh/sshd_config 8 | EXPOSE 22 9 | CMD ["/usr/sbin/sshd", "-D"] 10 | -------------------------------------------------------------------------------- /testing/docker/alpine-sshd-test-test-no-id-binary/sshd_config: -------------------------------------------------------------------------------- 1 | AuthorizedKeysFile .ssh/authorized_keys 2 | AllowTcpForwarding all 3 | DisableForwarding no 4 | GatewayPorts no 5 | X11Forwarding no 6 | Subsystem sftp /usr/lib/ssh/sftp-server 7 | -------------------------------------------------------------------------------- /testing/docker/alpine-sshd-test-test/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | RUN apk update 3 | RUN apk add openssh 4 | RUN ssh-keygen -A 5 | RUN echo -e "test\ntest"|adduser test 6 | EXPOSE 22 7 | CMD ["/usr/sbin/sshd", "-D"] 8 | --------------------------------------------------------------------------------