Merge pull request #101 from future-architect/external_ssh_mode
[WIP]Support scanning with external ssh command
This commit is contained in:
22
README.md
22
README.md
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -44,6 +44,8 @@ type Config struct {
|
||||
CvssScoreOver float64
|
||||
IgnoreUnscoredCves bool
|
||||
|
||||
SSHExternal bool
|
||||
|
||||
HTTPProxy string `valid:"url"`
|
||||
DBPath string
|
||||
CveDBPath string
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
170
scan/sshutil.go
170
scan/sshutil.go
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user