Skip to content

Commit

Permalink
Support scanning with external ssh command
Browse files Browse the repository at this point in the history
  • Loading branch information
kotakanbe committed Jun 21, 2016
1 parent 5e28ec2 commit 3cc852c
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 79 deletions.
10 changes: 10 additions & 0 deletions commands/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ type ScanCmd struct {
awsProfile string
awsS3Bucket string
awsRegion string

sshExternal bool
}

// Name return subcommand name
Expand All @@ -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]
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ type Config struct {
CvssScoreOver float64
IgnoreUnscoredCves bool

SSHExternal bool

HTTPProxy string `valid:"url"`
DBPath string
CveDBPath string
Expand Down
1 change: 1 addition & 0 deletions scan/freebsd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
69 changes: 33 additions & 36 deletions scan/serverapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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:
oses = append(oses, res)
if 0 < len(res.getErrs()) {
continue
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())
}
Log.Infof("(%d/%d) Detected %s: %s",
i+1, len(config.Conf.Servers),
res.getServerInfo().ServerName,
res.getDistributionInfo())
oses = append(oses, res)
case <-timeout:
msg := "Timeout occurred while detecting"
Log.Error(msg)
Expand All @@ -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
}
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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
}
Expand Down
170 changes: 127 additions & 43 deletions scan/sshutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ import (
"io/ioutil"
"net"
"os"
"os/exec"
"runtime"
"strings"
"syscall"
"time"

"golang.org/x/crypto/ssh"
Expand Down Expand Up @@ -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 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 runtime.GOOS == "windows" || !conf.Conf.SSHExternal {
return sshExecNative(c, cmd, sudo, log...)
}
return sshExecExternal(c, cmd, sudo, log...)
}

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()

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
}

Expand Down

0 comments on commit 3cc852c

Please sign in to comment.