diff --git a/README.md b/README.md index af3be4b1..2047fc9c 100644 --- a/README.md +++ b/README.md @@ -315,9 +315,7 @@ subjectPrefix = "[vuls]" [default] #port = "22" #user = "username" -#password = "password" #keyPath = "/home/username/.ssh/id_rsa" -#keyPassword = "password" [servers] @@ -325,9 +323,7 @@ subjectPrefix = "[vuls]" host = "172.31.4.82" #port = "22" #user = "root" -#password = "password" #keyPath = "/home/username/.ssh/id_rsa" -#keyPassword = "password" #cpeNames = [ # "cpe:/a:rubyonrails:ruby_on_rails:4.2.1", #] @@ -395,9 +391,7 @@ You can customize your configuration using this template. [default] #port = "22" #user = "username" - #password = "password" #keyPath = "/home/username/.ssh/id_rsa" - #keyPassword = "password" ``` Items of the default section will be used if not specified. @@ -409,9 +403,7 @@ You can customize your configuration using this template. host = "172.31.4.82" #port = "22" #user = "root" - #password = "password" #keyPath = "/home/username/.ssh/id_rsa" - #keyPassword = "password" #cpeNames = [ # "cpe:/a:rubyonrails:ruby_on_rails:4.2.1", #] @@ -440,9 +432,15 @@ Prepare subcommand installs required packages on each server. ``` $ vuls prepare -help -prepare: - prepare [-config=/path/to/config.toml] [-debug] +prepare + [-config=/path/to/config.toml] [-debug] + [-ask-sudo-password] + [-ask-key-password] + -ask-key-password + Ask ssh privatekey password before scanning + -ask-sudo-password + Ask sudo password of target servers before scanning -config string /path/to/toml (default "$PWD/config.toml") -debug @@ -456,6 +454,7 @@ prepare: # Usage: Scan ``` + $ vuls scan -help scan: scan @@ -467,8 +466,14 @@ scan: [-report-slack] [-report-mail] [-http-proxy=http://192.168.0.1:8080] + [-ask-sudo-password] + [-ask-key-password] [-debug] [-debug-sql] + -ask-key-password + Ask ssh privatekey password before scanning + -ask-sudo-password + Ask sudo password of target servers before scanning -config string /path/to/toml (default "$PWD/config.toml") -cve-dictionary-url string @@ -496,6 +501,21 @@ scan: ``` +## ask-key-password option + +| SSH key password | -ask-key-password || +|:-----------------|:-------------------|| +| empty password | - || +| with password | required | or use ssh-agent | + +## ask-sudo-password option + +| sudo password on target servers | -ask-sudo-password || +|:-----------------|:-------------------|| +| NOPASSWORD | - | defined as NOPASSWORD in /etc/sudoers on target servers | +| with password | required || + + ## example Run go-cve-dictionary as server mode before scanning. @@ -505,9 +525,10 @@ $ go-cve-dictionary server ### Scan all servers defined in config file ``` -$ vuls scan --report-slack --report-mail --cvss-over=7 +$ vuls scan --report-slack --report-mail --cvss-over=7 -ask-sudo-password -ask-key-password ``` With this sample command, it will .. +- Ask sudo password and ssh key passsword before scanning - Scan all servers defined in config file - Send scan results to slack and email - Only Report CVEs that CVSS score is over 7 @@ -518,7 +539,9 @@ With this sample command, it will .. $ vuls scan server1 server2 ``` With this sample command, it will .. -- Scan only 2 servers. (server1, server2) +- Use SSH Key-Based authentication with empty password (without -ask-key-password option) +- Sudo with no password (without -ask-sudo-password option) +- Scan only 2 servers (server1, server2) - Print scan result to terminal ---- @@ -594,6 +617,11 @@ Use Systemd, Upstart or supervisord, daemontools... - How to Enable Automatic-Update of Vunerability Data. Use job scheduler like Cron (with -last2y option). +- How to Enable Automatic-Scan. +Use job scheduler like Cron. +Set NOPASSWORD option in /etc/sudoers on target servers. +Use SSH Key-Based Authentication with empty password or ssh-agent. + - How to cross compile ```bash $ cd /path/to/your/local-git-reporsitory/vuls diff --git a/commands/cmdutil.go b/commands/cmdutil.go new file mode 100644 index 00000000..9a58019b --- /dev/null +++ b/commands/cmdutil.go @@ -0,0 +1,21 @@ +package commands + +import ( + "fmt" + + "github.com/howeyc/gopass" +) + +func getPasswd(prompt string) (string, error) { + for { + fmt.Print(prompt) + pass, err := gopass.GetPasswdMasked() + if err != nil { + return "", fmt.Errorf("Failed to read password") + } + if 0 < len(pass) { + return string(pass[:]), nil + } + } + +} diff --git a/commands/discover.go b/commands/discover.go index 6b68e526..bc84e211 100644 --- a/commands/discover.go +++ b/commands/discover.go @@ -111,9 +111,7 @@ subjectPrefix = "[vuls]" [default] #port = "22" #user = "username" -#password = "password" #keyPath = "/home/username/.ssh/id_rsa" -#keyPassword = "password" [servers] {{- $names:= .Names}} @@ -122,9 +120,7 @@ subjectPrefix = "[vuls]" host = "{{$ip}}" #port = "22" #user = "root" -#password = "password" #keyPath = "/home/username/.ssh/id_rsa" -#keyPassword = "password" #cpeNames = [ # "cpe:/a:rubyonrails:ruby_on_rails:4.2.1", #] diff --git a/commands/prepare.go b/commands/prepare.go index 5ca45324..d3daf9bb 100644 --- a/commands/prepare.go +++ b/commands/prepare.go @@ -34,6 +34,9 @@ type PrepareCmd struct { debug bool configPath string + askSudoPassword bool + askKeyPassword bool + useUnattendedUpgrades bool } @@ -55,7 +58,11 @@ func (*PrepareCmd) Synopsis() string { // Usage return usage func (*PrepareCmd) Usage() string { return `prepare: - prepare [-config=/path/to/config.toml] [-debug] + prepare + [-config=/path/to/config.toml] + [-ask-sudo-password] + [-ask-key-password] + [-debug] ` } @@ -68,6 +75,20 @@ func (p *PrepareCmd) SetFlags(f *flag.FlagSet) { defaultConfPath := os.Getenv("PWD") + "/config.toml" f.StringVar(&p.configPath, "config", defaultConfPath, "/path/to/toml") + f.BoolVar( + &p.askKeyPassword, + "ask-key-password", + false, + "Ask ssh privatekey password before scanning", + ) + + f.BoolVar( + &p.askSudoPassword, + "ask-sudo-password", + false, + "Ask sudo password of target servers before scanning", + ) + f.BoolVar( &p.useUnattendedUpgrades, "use-unattended-upgrades", @@ -78,14 +99,30 @@ func (p *PrepareCmd) SetFlags(f *flag.FlagSet) { // Execute execute func (p *PrepareCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { - logrus.Infof("Start Preparing (config: %s)", p.configPath) + var keyPass, sudoPass string + var err error + if p.askKeyPassword { + prompt := "SSH key password: " + if keyPass, err = getPasswd(prompt); err != nil { + logrus.Error(err) + return subcommands.ExitFailure + } + } + if p.askSudoPassword { + prompt := "sudo password: " + if sudoPass, err = getPasswd(prompt); err != nil { + logrus.Error(err) + return subcommands.ExitFailure + } + } - err := c.Load(p.configPath) + err = c.Load(p.configPath, keyPass, sudoPass) if err != nil { logrus.Errorf("Error loading %s, %s", p.configPath, err) return subcommands.ExitUsageError } + logrus.Infof("Start Preparing (config: %s)", p.configPath) target := make(map[string]c.ServerInfo) for _, arg := range f.Args() { found := false diff --git a/commands/scan.go b/commands/scan.go index 137b4150..272a29f5 100644 --- a/commands/scan.go +++ b/commands/scan.go @@ -45,12 +45,15 @@ type ScanCmd struct { cvssScoreOver float64 httpProxy string - useYumPluginSecurity bool - useUnattendedUpgrades bool - // reporting reportSlack bool reportMail bool + + askSudoPassword bool + askKeyPassword bool + + useYumPluginSecurity bool + useUnattendedUpgrades bool } // Name return subcommand name @@ -71,6 +74,8 @@ func (*ScanCmd) Usage() string { [-report-slack] [-report-mail] [-http-proxy=http://192.168.0.1:8080] + [-ask-sudo-password] + [-ask-key-password] [-debug] [-debug-sql] ` @@ -111,6 +116,20 @@ func (p *ScanCmd) SetFlags(f *flag.FlagSet) { f.BoolVar(&p.reportSlack, "report-slack", false, "Slack report") f.BoolVar(&p.reportMail, "report-mail", false, "Email report") + f.BoolVar( + &p.askKeyPassword, + "ask-key-password", + false, + "Ask ssh privatekey password before scanning", + ) + + f.BoolVar( + &p.askSudoPassword, + "ask-sudo-password", + false, + "Ask sudo password of target servers before scanning", + ) + f.BoolVar( &p.useYumPluginSecurity, "use-yum-plugin-security", @@ -129,14 +148,30 @@ func (p *ScanCmd) SetFlags(f *flag.FlagSet) { // Execute execute func (p *ScanCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + var keyPass, sudoPass string + var err error + if p.askKeyPassword { + prompt := "SSH key password: " + if keyPass, err = getPasswd(prompt); err != nil { + logrus.Error(err) + return subcommands.ExitFailure + } + } + if p.askSudoPassword { + prompt := "sudo password: " + if sudoPass, err = getPasswd(prompt); err != nil { + logrus.Error(err) + return subcommands.ExitFailure + } + } - logrus.Infof("Start scanning (config: %s)", p.configPath) - err := c.Load(p.configPath) + err = c.Load(p.configPath, keyPass, sudoPass) if err != nil { logrus.Errorf("Error loading %s, %s", p.configPath, err) return subcommands.ExitUsageError } + logrus.Infof("Start scanning (config: %s)", p.configPath) target := make(map[string]c.ServerInfo) for _, arg := range f.Args() { found := false diff --git a/config/jsonloader.go b/config/jsonloader.go index a5a1e406..a3bb5432 100644 --- a/config/jsonloader.go +++ b/config/jsonloader.go @@ -24,6 +24,6 @@ type JSONLoader struct { } // Load load the configuraiton JSON file specified by path arg. -func (c JSONLoader) Load(path string) (err error) { +func (c JSONLoader) Load(path, sudoPass, keyPass string) (err error) { return fmt.Errorf("Not implement yet") } diff --git a/config/loader.go b/config/loader.go index bb1151cb..d157f8bb 100644 --- a/config/loader.go +++ b/config/loader.go @@ -18,16 +18,14 @@ along with this program. If not, see . package config // Load loads configuration -func Load(path string) error { - - //TODO if path's suffix .toml +func Load(path, keyPass, sudoPass string) error { var loader Loader loader = TOMLLoader{} - return loader.Load(path) + return loader.Load(path, keyPass, sudoPass) } // Loader is interface of concrete loader type Loader interface { - Load(string) error + Load(string, string, string) error } diff --git a/config/tomlloader.go b/config/tomlloader.go index 3339ade6..6b031f63 100644 --- a/config/tomlloader.go +++ b/config/tomlloader.go @@ -31,7 +31,7 @@ type TOMLLoader struct { } // Load load the configuraiton TOML file specified by path arg. -func (c TOMLLoader) Load(pathToToml string) (err error) { +func (c TOMLLoader) Load(pathToToml, keyPass, sudoPass string) (err error) { var conf Config if _, err := toml.DecodeFile(pathToToml, &conf); err != nil { log.Error("Load config failed", err) @@ -45,14 +45,28 @@ func (c TOMLLoader) Load(pathToToml string) (err error) { Conf.Default = d servers := make(map[string]ServerInfo) + if keyPass != "" { + d.KeyPassword = keyPass + } + + if sudoPass != "" { + d.Password = sudoPass + } + i := 0 for name, v := range conf.Servers { + + if 0 < len(v.KeyPassword) || 0 < len(v.Password) { + log.Warn("[Depricated] password and keypassword in config file are unsecure. Remove them immediately for a security reason. They will be removed in a future release.") + } + s := ServerInfo{ServerName: name} s.User = v.User if s.User == "" { s.User = d.User } + // s.Password = sudoPass s.Password = v.Password if s.Password == "" { s.Password = d.Password @@ -76,6 +90,7 @@ func (c TOMLLoader) Load(pathToToml string) (err error) { } } + // s.KeyPassword = keyPass s.KeyPassword = v.KeyPassword if s.KeyPassword == "" { s.KeyPassword = d.KeyPassword diff --git a/scan/redhat.go b/scan/redhat.go index 43507435..dfc46e75 100644 --- a/scan/redhat.go +++ b/scan/redhat.go @@ -129,6 +129,7 @@ func (o *redhat) installYumPluginSecurity() error { return nil } + o.log.Info("Installing yum-plugin-security...") cmd := util.PrependProxyEnv("yum install -y yum-plugin-security") if r := o.ssh(cmd, sudo); !r.isSuccess() { return fmt.Errorf( @@ -139,7 +140,6 @@ func (o *redhat) installYumPluginSecurity() error { } func (o *redhat) installYumChangelog() error { - o.log.Info("Installing yum-plugin-security...") if o.Family == "centos" { var majorVersion int @@ -164,6 +164,7 @@ func (o *redhat) installYumChangelog() error { return nil } + o.log.Infof("Installing %s...", packName) cmd = util.PrependProxyEnv("yum install -y " + packName) if r := o.ssh(cmd, sudo); !r.isSuccess() { return fmt.Errorf( @@ -458,7 +459,10 @@ func (o *redhat) parseYumCheckUpdateLine(line string) (models.PackageInfo, error } func (o *redhat) getChangelog(packageNames string) (stdout string, err error) { - command := "echo N | " + command := "" + if o.ServerInfo.User == "root" { + command = "yes no | " + } if 0 < len(config.Conf.HTTPProxy) { command += util.ProxyEnv() } diff --git a/scan/serverapi.go b/scan/serverapi.go index 8f116650..dccecd74 100644 --- a/scan/serverapi.go +++ b/scan/serverapi.go @@ -7,7 +7,6 @@ import ( "github.com/Sirupsen/logrus" "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" - "github.com/k0kubun/pp" cve "github.com/kotakanbe/go-cve-dictionary/models" ) @@ -110,8 +109,6 @@ func InitServers(localLogger *logrus.Entry) (err error) { Log = localLogger if servers, err = detectServersOS(); err != nil { err = fmt.Errorf("Failed to detect the type of OS. err: %s", err) - } else { - Log.Debugf("%s", pp.Sprintf("%s", servers)) } return } diff --git a/scan/sshutil.go b/scan/sshutil.go index 6d0acd2e..df39c49a 100644 --- a/scan/sshutil.go +++ b/scan/sshutil.go @@ -137,7 +137,8 @@ func sshExec(c conf.ServerInfo, cmd string, sudo bool, log ...*logrus.Entry) (re // set pipefail option. // http://unix.stackexchange.com/questions/14270/get-exit-status-of-process-thats-piped-to-another cmd = fmt.Sprintf("set -o pipefail; %s", cmd) - logger.Debugf("Command: %s", strings.Replace(cmd, "\n", "", -1)) + logger.Debugf("Command: %s", + strings.Replace(maskPassword(cmd, c.Password), "\n", "", -1)) var client *ssh.Client client, err = sshConnect(c) @@ -189,7 +190,7 @@ func sshExec(c conf.ServerInfo, cmd string, sudo bool, log ...*logrus.Entry) (re logger.Debugf( "SSH executed. cmd: %s, status: %#v\nstdout: \n%s\nstderr: \n%s", - cmd, err, result.Stdout, result.Stderr) + maskPassword(cmd, c.Password), err, result.Stdout, result.Stderr) return } @@ -225,7 +226,8 @@ func sshConnect(c conf.ServerInfo) (client *ssh.Client, err error) { var auths = []ssh.AuthMethod{} if auths, err = addKeyAuth(auths, c.KeyPath, c.KeyPassword); err != nil { - logrus.Fatalf("Failed to add keyAuth. err: %s", err) + logrus.Fatalf("Failed to add keyAuth. %s@%s:%s err: %s", + c.User, c.Host, c.Port, err) } if c.Password != "" { @@ -237,12 +239,10 @@ func sshConnect(c conf.ServerInfo) (client *ssh.Client, err error) { User: c.User, Auth: auths, } - // log.Debugf("config: %s", pp.Sprintf("%v", config)) notifyFunc := func(e error, t time.Duration) { - logrus.Warnf("Failed to ssh %s@%s:%s. err: %s, Retrying in %s...", + logrus.Warnf("Failed to ssh %s@%s:%s err: %s, Retrying in %s...", c.User, c.Host, c.Port, e, t) - logrus.Debugf("sshConInfo: %s", pp.Sprintf("%v", c)) } err = backoff.RetryNotify(func() error { if client, err = ssh.Dial("tcp", c.Host+":"+c.Port, config); err != nil { @@ -309,3 +309,8 @@ func parsePemBlock(block *pem.Block) (interface{}, error) { return nil, fmt.Errorf("rtop: unsupported key type %q", block.Type) } } + +// ref golang.org/x/crypto/ssh/keys.go#ParseRawPrivateKey. +func maskPassword(cmd, sudoPass string) string { + return strings.Replace(cmd, fmt.Sprintf("echo %s", sudoPass), "echo *****", -1) +}