Support scanning with external ssh command

This commit is contained in:
kota kanbe
2016-06-16 19:59:30 +09:00
parent 5e28ec22e1
commit 0ef1a5a3ce
6 changed files with 195 additions and 81 deletions

View File

@@ -427,12 +427,18 @@ You can customize your configuration using this template.
#]
#containers = ["${running}"]
```
You can overwrite the default value specified in default section.
Vuls supports multiple SSH authentication methods.
Vuls supports two types of SSH. One is native go implementation. The other is external SSH command. For details, see [-ssh-external option](https://github.com/future-architect/vuls#-ssh-external-option)
Multiple SSH authentication methods are supported.
- SSH agent
- SSH public key authentication (with password, empty password)
- Password authentication
----
# Usage: Prepare
@@ -484,6 +490,7 @@ scan:
[-cve-dictionary-url=http://127.0.0.1:1323]
[-cvss-over=7]
[-ignore-unscored-cves]
[-ssh-external]
[-report-json]
[-report-mail]
[-report-s3]
@@ -538,6 +545,8 @@ scan:
Send report via Slack
-report-text
Write report to text files ($PWD/results/current)
-ssh-external
Use external ssh command. Default: Use the Go native implementation
-use-unattended-upgrades
[Deprecated] For Ubuntu. Scan by unattended-upgrades or not (use apt-get upgrade --dry-run by default)
-use-yum-plugin-security
@@ -545,6 +554,16 @@ scan:
```
## -ssh-external option
Vuls supports different types of SSH.
By Defaut, using a native Go implementation from crypto/ssh.
This is useful in situations where you may not have access to traditional UNIX tools.
To use external ssh command, specify this option.
This is useful If you want to use ProxyCommand or chiper algorithm of SSH that is not supported by native go implementation.
## -ask-key-password option
| SSH key password | -ask-key-password | |
@@ -559,6 +578,7 @@ scan:
| NOPASSWORD | - | defined as NOPASSWORD in /etc/sudoers on target servers |
| with password | required | . |
## -report-json , -report-text option
At the end of the scan, scan results will be available in the $PWD/result/current/ directory.

View File

@@ -67,6 +67,8 @@ type ScanCmd struct {
awsProfile string
awsS3Bucket string
awsRegion string
sshExternal bool
}
// Name return subcommand name
@@ -86,6 +88,7 @@ func (*ScanCmd) Usage() string {
[-cve-dictionary-url=http://127.0.0.1:1323]
[-cvss-over=7]
[-ignore-unscored-cves]
[-ssh-external]
[-report-json]
[-report-mail]
[-report-s3]
@@ -141,6 +144,12 @@ func (p *ScanCmd) SetFlags(f *flag.FlagSet) {
false,
"Don't report the unscored CVEs")
f.BoolVar(
&p.sshExternal,
"ssh-external",
false,
"Use external ssh command. Default: Use the Go native implementation")
f.StringVar(
&p.httpProxy,
"http-proxy",
@@ -292,6 +301,7 @@ func (p *ScanCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
c.Conf.CveDictionaryURL = p.cveDictionaryURL
c.Conf.CvssScoreOver = p.cvssScoreOver
c.Conf.IgnoreUnscoredCves = p.ignoreUnscoredCves
c.Conf.SSHExternal = p.sshExternal
c.Conf.HTTPProxy = p.httpProxy
c.Conf.UseYumPluginSecurity = p.useYumPluginSecurity
c.Conf.UseUnattendedUpgrades = p.useUnattendedUpgrades

View File

@@ -44,6 +44,8 @@ type Config struct {
CvssScoreOver float64
IgnoreUnscoredCves bool
SSHExternal bool
HTTPProxy string `valid:"url"`
DBPath string
CveDBPath string

View File

@@ -35,6 +35,7 @@ func detectFreebsd(c config.ServerInfo) (itsMe bool, bsd osTypeInterface) {
}
}
}
Log.Debugf("Not FreeBSD. Host: %s:%s", c.Host, c.Port)
return false, bsd
}

View File

@@ -108,19 +108,19 @@ func (s CvePacksList) Less(i, j int) bool {
func detectOS(c config.ServerInfo) (osType osTypeInterface) {
var itsMe bool
itsMe, osType = detectDebian(c)
if itsMe {
if itsMe, osType = detectDebian(c); itsMe {
Log.Debugf("Debian like Linux. Host: %s:%s", c.Host, c.Port)
return
}
itsMe, osType = detectRedhat(c)
if itsMe {
if itsMe, osType = detectRedhat(c); itsMe {
Log.Debugf("Redhat like Linux. Host: %s:%s", c.Host, c.Port)
return
}
itsMe, osType = detectFreebsd(c)
if itsMe {
if itsMe, osType = detectFreebsd(c); itsMe {
Log.Debugf("FreeBSD. Host: %s:%s", c.Host, c.Port)
return
}
osType.setServerInfo(c)
osType.setErrs([]error{fmt.Errorf("Unknown OS Type")})
return
}
@@ -177,18 +177,21 @@ func detectServerOSes() (oses []osTypeInterface, err error) {
}(s)
}
timeout := time.After(300 * time.Second)
timeout := time.After(60 * time.Second)
for i := 0; i < len(config.Conf.Servers); i++ {
select {
case res := <-osTypeChan:
if 0 < len(res.getErrs()) {
continue
}
Log.Infof("(%d/%d) Detected %s: %s",
i+1, len(config.Conf.Servers),
res.getServerInfo().ServerName,
res.getDistributionInfo())
oses = append(oses, res)
if 0 < len(res.getErrs()) {
Log.Infof("(%d/%d) Failed %s",
i+1, len(config.Conf.Servers),
res.getServerInfo().ServerName)
} else {
Log.Infof("(%d/%d) Detected %s: %s",
i+1, len(config.Conf.Servers),
res.getServerInfo().ServerName,
res.getDistributionInfo())
}
case <-timeout:
msg := "Timeout occurred while detecting"
Log.Error(msg)
@@ -208,19 +211,16 @@ func detectServerOSes() (oses []osTypeInterface, err error) {
}
}
errorOccurred := false
errs := []error{}
for _, osi := range oses {
if errs := osi.getErrs(); 0 < len(errs) {
errorOccurred = true
Log.Errorf("Some errors occurred on %s",
osi.getServerInfo().ServerName)
for _, err := range errs {
Log.Error(err)
}
if 0 < len(osi.getErrs()) {
errs = append(errs, fmt.Errorf(
"Error occurred on %s. errs: %s",
osi.getServerInfo().ServerName, osi.getErrs()))
}
}
if errorOccurred {
return oses, fmt.Errorf("Some errors occurred")
if 0 < len(errs) {
return oses, fmt.Errorf("%s", errs)
}
return
}
@@ -234,10 +234,11 @@ func detectContainerOSes() (oses []osTypeInterface, err error) {
}(s)
}
timeout := time.After(300 * time.Second)
timeout := time.After(60 * time.Second)
for i := 0; i < len(config.Conf.Servers); i++ {
select {
case res := <-osTypesChan:
oses = append(oses, res...)
for _, osi := range res {
if 0 < len(osi.getErrs()) {
continue
@@ -247,7 +248,6 @@ func detectContainerOSes() (oses []osTypeInterface, err error) {
sinfo.Container.ContainerID, sinfo.Container.Name,
sinfo.ServerName, osi.getDistributionInfo())
}
oses = append(oses, res...)
case <-timeout:
msg := "Timeout occurred while detecting"
Log.Error(msg)
@@ -267,19 +267,16 @@ func detectContainerOSes() (oses []osTypeInterface, err error) {
}
}
errorOccurred := false
errs := []error{}
for _, osi := range oses {
if errs := osi.getErrs(); 0 < len(errs) {
errorOccurred = true
Log.Errorf("Some errors occurred on %s",
osi.getServerInfo().ServerName)
for _, err := range errs {
Log.Error(err)
}
if 0 < len(osi.getErrs()) {
errs = append(errs, fmt.Errorf(
"Error occurred on %s. errs: %s",
osi.getServerInfo().ServerName, osi.getErrs()))
}
}
if errorOccurred {
return oses, fmt.Errorf("Some errors occurred")
if 0 < len(errs) {
return oses, fmt.Errorf("%s", errs)
}
return
}

View File

@@ -25,7 +25,10 @@ import (
"io/ioutil"
"net"
"os"
"os/exec"
"runtime"
"strings"
"syscall"
"time"
"golang.org/x/crypto/ssh"
@@ -105,53 +108,21 @@ func parallelSSHExec(fn func(osTypeInterface) error, timeoutSec ...int) (errs []
}
func sshExec(c conf.ServerInfo, cmd string, sudo bool, log ...*logrus.Entry) (result sshResult) {
// Setup Logger
var logger *logrus.Entry
if len(log) == 0 {
level := logrus.InfoLevel
if conf.Conf.Debug == true {
level = logrus.DebugLevel
}
l := &logrus.Logger{
Out: os.Stderr,
Formatter: new(logrus.TextFormatter),
Hooks: make(logrus.LevelHooks),
Level: level,
}
logger = logrus.NewEntry(l)
} else {
logger = log[0]
}
c.SudoOpt.ExecBySudo = true
var err error
if sudo && c.User != "root" && !c.IsContainer() {
switch {
case c.SudoOpt.ExecBySudo:
cmd = fmt.Sprintf("echo %s | sudo -S %s", c.Password, cmd)
case c.SudoOpt.ExecBySudoSh:
cmd = fmt.Sprintf("echo %s | sudo sh -c '%s'", c.Password, cmd)
default:
logger.Panicf("sudoOpt is invalid. SudoOpt: %v", c.SudoOpt)
}
if runtime.GOOS == "windows" || !conf.Conf.SSHExternal {
return sshExecNative(c, cmd, sudo, log...)
}
return sshExecExternal(c, cmd, sudo, log...)
}
if c.Family != "FreeBSD" {
// set pipefail option. Bash only
// http://unix.stackexchange.com/questions/14270/get-exit-status-of-process-thats-piped-to-another
cmd = fmt.Sprintf("set -o pipefail; %s", cmd)
}
if c.IsContainer() {
switch c.Container.Type {
case "", "docker":
cmd = fmt.Sprintf(`docker exec %s /bin/bash -c "%s"`, c.Container.ContainerID, cmd)
}
}
func sshExecNative(c conf.ServerInfo, cmd string, sudo bool, log ...*logrus.Entry) (result sshResult) {
logger := getSSHLogger(log...)
cmd = decolateCmd(c, cmd, sudo)
logger.Debugf("Command: %s",
strings.Replace(maskPassword(cmd, c.Password), "\n", "", -1))
var client *ssh.Client
var err error
client, err = sshConnect(c)
defer client.Close()
@@ -200,12 +171,125 @@ func sshExec(c conf.ServerInfo, cmd string, sudo bool, log ...*logrus.Entry) (re
result.Port = c.Port
logger.Debugf(
"SSH executed. cmd: %s, status: %#v\nstdout: \n%s\nstderr: \n%s",
maskPassword(cmd, c.Password), err, result.Stdout, result.Stderr)
"SSH executed. cmd: %s, err: %#v, status: %d\nstdout: \n%s\nstderr: \n%s",
maskPassword(cmd, c.Password), err, result.ExitStatus, result.Stdout, result.Stderr)
return
}
func sshExecExternal(c conf.ServerInfo, cmd string, sudo bool, log ...*logrus.Entry) (result sshResult) {
logger := getSSHLogger(log...)
sshBinaryPath, err := exec.LookPath("ssh")
if err != nil {
logger.Debug("Failed to find SSH binary, using native Go implementation")
return sshExecNative(c, cmd, sudo, log...)
}
defaultSSHArgs := []string{
"-o", "StrictHostKeyChecking=no",
"-o", "UserKnownHostsFile=/dev/null",
"-o", "LogLevel=quiet",
"-o", "ConnectionAttempts=3",
"-o", "ConnectTimeout=10",
"-o", "ControlMaster=no",
"-o", "ControlPath=none",
// TODO ssh session multiplexing
// "-o", "ControlMaster=auto",
// "-o", `ControlPath=~/.ssh/controlmaster-%r-%h.%p`,
// "-o", "Controlpersist=30m",
}
args := append(defaultSSHArgs, fmt.Sprintf("%s@%s", c.User, c.Host))
args = append(args, "-p", c.Port)
// if conf.Conf.Debug {
// args = append(args, "-v")
// }
if 0 < len(c.KeyPath) {
args = append(args, "-i", c.KeyPath)
args = append(args, "-o", "PasswordAuthentication=no")
}
cmd = decolateCmd(c, cmd, sudo)
args = append(args, cmd)
execCmd := exec.Command(sshBinaryPath, args...)
var stdoutBuf, stderrBuf bytes.Buffer
execCmd.Stdout = &stdoutBuf
execCmd.Stderr = &stderrBuf
if err := execCmd.Run(); err != nil {
if e, ok := err.(*exec.ExitError); ok {
if s, ok := e.Sys().(syscall.WaitStatus); ok {
result.ExitStatus = s.ExitStatus()
} else {
result.ExitStatus = 998
}
} else {
result.ExitStatus = 999
}
} else {
result.ExitStatus = 0
}
result.Stdout = stdoutBuf.String()
result.Stderr = stderrBuf.String()
result.Host = c.Host
result.Port = c.Port
logger.Debugf(
"SSH executed. cmd: %s %s, err: %#v, status: %d\nstdout: \n%s\nstderr: \n%s",
sshBinaryPath,
maskPassword(strings.Join(args, " "), c.Password),
err, result.ExitStatus, result.Stdout, result.Stderr)
return
}
func getSSHLogger(log ...*logrus.Entry) *logrus.Entry {
if len(log) == 0 {
level := logrus.InfoLevel
if conf.Conf.Debug == true {
level = logrus.DebugLevel
}
l := &logrus.Logger{
Out: os.Stderr,
Formatter: new(logrus.TextFormatter),
Hooks: make(logrus.LevelHooks),
Level: level,
}
return logrus.NewEntry(l)
}
return log[0]
}
func decolateCmd(c conf.ServerInfo, cmd string, sudo bool) string {
c.SudoOpt.ExecBySudo = true
if sudo && c.User != "root" && !c.IsContainer() {
switch {
case c.SudoOpt.ExecBySudo:
cmd = fmt.Sprintf("echo %s | sudo -S %s", c.Password, cmd)
case c.SudoOpt.ExecBySudoSh:
cmd = fmt.Sprintf("echo %s | sudo sh -c '%s'", c.Password, cmd)
}
}
if c.Family != "FreeBSD" {
// set pipefail option. Bash only
// http://unix.stackexchange.com/questions/14270/get-exit-status-of-process-thats-piped-to-another
cmd = fmt.Sprintf("set -o pipefail; %s", cmd)
}
if c.IsContainer() {
switch c.Container.Type {
case "", "docker":
cmd = fmt.Sprintf(`docker exec %s /bin/bash -c "%s"`, c.Container.ContainerID, cmd)
}
}
return cmd
}
func getAgentAuth() (auth ssh.AuthMethod, ok bool) {
if sock := os.Getenv("SSH_AUTH_SOCK"); len(sock) > 0 {
if agconn, err := net.Dial("unix", sock); err == nil {
@@ -317,7 +401,7 @@ func parsePemBlock(block *pem.Block) (interface{}, error) {
case "DSA PRIVATE KEY":
return ssh.ParseDSAPrivateKey(block.Bytes)
default:
return nil, fmt.Errorf("rtop: unsupported key type %q", block.Type)
return nil, fmt.Errorf("Unsupported key type %q", block.Type)
}
}