From 768ffdf61039aeacbec0c91c3acfbab8e0e0991f Mon Sep 17 00:00:00 2001 From: Kota Kanbe Date: Thu, 12 Jan 2017 16:35:11 +0900 Subject: [PATCH] Add local scan mode. If the scan target server is localhost, Don't use SSH. #210 --- scan/base.go | 14 +++++------ scan/debian.go | 30 +++++++++++----------- scan/freebsd.go | 10 ++++---- scan/redhat.go | 32 ++++++++++++------------ scan/serverapi.go | 3 +++ scan/sshutil.go | 63 ++++++++++++++++++++++++++++++++++++----------- 6 files changed, 94 insertions(+), 58 deletions(-) diff --git a/scan/base.go b/scan/base.go index 70c0570983..064c2b4212 100644 --- a/scan/base.go +++ b/scan/base.go @@ -41,8 +41,8 @@ type base struct { errs []error } -func (l *base) ssh(cmd string, sudo bool) sshResult { - return sshExec(l.ServerInfo, cmd, sudo, l.log) +func (l *base) exec(cmd string, sudo bool) execResult { + return exec(l.ServerInfo, cmd, sudo, l.log) } func (l *base) setServerInfo(c config.ServerInfo) { @@ -125,7 +125,7 @@ func (l *base) exitedContainers() (containers []config.Container, err error) { func (l *base) dockerPs(option string) (string, error) { cmd := fmt.Sprintf("docker ps %s", option) - r := l.ssh(cmd, noSudo) + r := l.exec(cmd, noSudo) if !r.isSuccess() { return "", fmt.Errorf("Failed to SSH: %s", r) } @@ -171,9 +171,9 @@ func (l *base) detectPlatform() error { } func (l base) detectRunningOnAws() (ok bool, instanceID string, err error) { - if r := l.ssh("type curl", noSudo); r.isSuccess() { + if r := l.exec("type curl", noSudo); r.isSuccess() { cmd := "curl --max-time 1 --retry 3 --noproxy 169.254.169.254 http://169.254.169.254/latest/meta-data/instance-id" - r := l.ssh(cmd, noSudo) + r := l.exec(cmd, noSudo) if r.isSuccess() { id := strings.TrimSpace(r.Stdout) if !l.isAwsInstanceID(id) { @@ -191,9 +191,9 @@ func (l base) detectRunningOnAws() (ok bool, instanceID string, err error) { } } - if r := l.ssh("type wget", noSudo); r.isSuccess() { + if r := l.exec("type wget", noSudo); r.isSuccess() { cmd := "wget --tries=3 --timeout=1 --no-proxy -q -O - http://169.254.169.254/latest/meta-data/instance-id" - r := l.ssh(cmd, noSudo) + r := l.exec(cmd, noSudo) if r.isSuccess() { id := strings.TrimSpace(r.Stdout) if !l.isAwsInstanceID(id) { diff --git a/scan/debian.go b/scan/debian.go index 22f4248d1a..26c5c77297 100644 --- a/scan/debian.go +++ b/scan/debian.go @@ -48,9 +48,9 @@ func newDebian(c config.ServerInfo) *debian { func detectDebian(c config.ServerInfo) (itsMe bool, deb osTypeInterface, err error) { deb = newDebian(c) - if r := sshExec(c, "ls /etc/debian_version", noSudo); !r.isSuccess() { + if r := exec(c, "ls /etc/debian_version", noSudo); !r.isSuccess() { if r.Error != nil { - return false, deb, r.Error + return false, deb, nil } if r.ExitStatus == 255 { return false, deb, fmt.Errorf( @@ -60,7 +60,7 @@ func detectDebian(c config.ServerInfo) (itsMe bool, deb osTypeInterface, err err return false, deb, nil } - if r := sshExec(c, "lsb_release -ir", noSudo); r.isSuccess() { + if r := exec(c, "lsb_release -ir", noSudo); r.isSuccess() { // e.g. // root@fa3ec524be43:/# lsb_release -ir // Distributor ID: Ubuntu @@ -79,7 +79,7 @@ func detectDebian(c config.ServerInfo) (itsMe bool, deb osTypeInterface, err err return true, deb, nil } - if r := sshExec(c, "cat /etc/lsb-release", noSudo); r.isSuccess() { + if r := exec(c, "cat /etc/lsb-release", noSudo); r.isSuccess() { // e.g. // DISTRIB_ID=Ubuntu // DISTRIB_RELEASE=14.04 @@ -100,7 +100,7 @@ func detectDebian(c config.ServerInfo) (itsMe bool, deb osTypeInterface, err err // Debian cmd := "cat /etc/debian_version" - if r := sshExec(c, cmd, noSudo); r.isSuccess() { + if r := exec(c, cmd, noSudo); r.isSuccess() { deb.setDistro("debian", trim(r.Stdout)) return true, deb, nil } @@ -114,7 +114,7 @@ func trim(str string) string { } func (o *debian) checkIfSudoNoPasswd() error { - r := o.ssh("apt-get -v", sudo) + r := o.exec("apt-get -v", sudo) if !r.isSuccess() { o.log.Errorf("sudo error on %s", r) return fmt.Errorf("Failed to sudo: %s", r) @@ -133,7 +133,7 @@ func (o *debian) checkDependencies() error { // Because unable to get changelogs via apt-get changelog on Debian. name := "aptitude" cmd := name + " -h" - if r := o.ssh(cmd, noSudo); !r.isSuccess() { + if r := o.exec(cmd, noSudo); !r.isSuccess() { o.lackDependencies = []string{name} } return nil @@ -151,7 +151,7 @@ func (o *debian) install() error { // apt-get update o.log.Infof("apt-get update...") cmd := util.PrependProxyEnv("apt-get update") - if r := o.ssh(cmd, sudo); !r.isSuccess() { + if r := o.exec(cmd, sudo); !r.isSuccess() { msg := fmt.Sprintf("Failed to SSH: %s", r) o.log.Errorf(msg) return fmt.Errorf(msg) @@ -159,7 +159,7 @@ func (o *debian) install() error { for _, name := range o.lackDependencies { cmd = util.PrependProxyEnv("apt-get install -y " + name) - if r := o.ssh(cmd, sudo); !r.isSuccess() { + if r := o.exec(cmd, sudo); !r.isSuccess() { msg := fmt.Sprintf("Failed to SSH: %s", r) o.log.Errorf(msg) return fmt.Errorf(msg) @@ -188,7 +188,7 @@ func (o *debian) scanPackages() error { } func (o *debian) scanInstalledPackages() (packs []models.PackageInfo, err error) { - r := o.ssh("dpkg-query -W", noSudo) + r := o.exec("dpkg-query -W", noSudo) if !r.isSuccess() { return packs, fmt.Errorf("Failed to SSH: %s", r) } @@ -232,7 +232,7 @@ func (o *debian) parseScannedPackagesLine(line string) (name, version string, er func (o *debian) checkRequiredPackagesInstalled() error { if o.Distro.Family == "debian" { - if r := o.ssh("test -f /usr/bin/aptitude", noSudo); !r.isSuccess() { + if r := o.exec("test -f /usr/bin/aptitude", noSudo); !r.isSuccess() { msg := fmt.Sprintf("aptitude is not installed: %s", r) o.log.Errorf(msg) return fmt.Errorf(msg) @@ -244,7 +244,7 @@ func (o *debian) checkRequiredPackagesInstalled() error { func (o *debian) scanUnsecurePackages(installed []models.PackageInfo) ([]models.VulnInfo, error) { o.log.Infof("apt-get update...") cmd := util.PrependProxyEnv("apt-get update") - if r := o.ssh(cmd, sudo); !r.isSuccess() { + if r := o.exec(cmd, sudo); !r.isSuccess() { return nil, fmt.Errorf("Failed to SSH: %s", r) } @@ -328,7 +328,7 @@ func (o *debian) fillCandidateVersion(before models.PackageInfoList) (filled []m names = append(names, p.Name) } cmd := fmt.Sprintf("LANGUAGE=en_US.UTF-8 apt-cache policy %s", strings.Join(names, " ")) - r := o.ssh(cmd, sudo) + r := o.exec(cmd, sudo) if !r.isSuccess() { return nil, fmt.Errorf("Failed to SSH: %s", r) } @@ -350,7 +350,7 @@ func (o *debian) fillCandidateVersion(before models.PackageInfoList) (filled []m func (o *debian) GetUpgradablePackNames() (packNames []string, err error) { cmd := util.PrependProxyEnv("LANGUAGE=en_US.UTF-8 apt-get upgrade --dry-run") - r := o.ssh(cmd, sudo) + r := o.exec(cmd, sudo) if r.isSuccess(0, 1) { return o.parseAptGetUpgrade(r.Stdout) } @@ -529,7 +529,7 @@ func (o *debian) scanPackageCveIDs(pack models.PackageInfo) ([]string, error) { } cmd = util.PrependProxyEnv(cmd) - r := o.ssh(cmd, noSudo) + r := o.exec(cmd, noSudo) if !r.isSuccess() { o.log.Warnf("Failed to SSH: %s", r) // Ignore this Error. diff --git a/scan/freebsd.go b/scan/freebsd.go index 0abee79e6c..e1ffe5c5f0 100644 --- a/scan/freebsd.go +++ b/scan/freebsd.go @@ -46,9 +46,9 @@ func detectFreebsd(c config.ServerInfo) (itsMe bool, bsd osTypeInterface) { // Prevent from adding `set -o pipefail` option c.Distro = config.Distro{Family: "FreeBSD"} - if r := sshExec(c, "uname", noSudo); r.isSuccess() { + if r := exec(c, "uname", noSudo); r.isSuccess() { if strings.Contains(r.Stdout, "FreeBSD") == true { - if b := sshExec(c, "uname -r", noSudo); b.isSuccess() { + if b := exec(c, "uname -r", noSudo); b.isSuccess() { rel := strings.TrimSpace(b.Stdout) bsd.setDistro("FreeBSD", rel) return true, bsd @@ -97,7 +97,7 @@ func (o *bsd) scanPackages() error { func (o *bsd) scanInstalledPackages() ([]models.PackageInfo, error) { cmd := util.PrependProxyEnv("pkg version -v") - r := o.ssh(cmd, noSudo) + r := o.exec(cmd, noSudo) if !r.isSuccess() { return nil, fmt.Errorf("Failed to SSH: %s", r) } @@ -107,13 +107,13 @@ func (o *bsd) scanInstalledPackages() ([]models.PackageInfo, error) { func (o *bsd) scanUnsecurePackages() (vulnInfos []models.VulnInfo, err error) { const vulndbPath = "/tmp/vuln.db" cmd := "rm -f " + vulndbPath - r := o.ssh(cmd, noSudo) + r := o.exec(cmd, noSudo) if !r.isSuccess(0) { return nil, fmt.Errorf("Failed to SSH: %s", r) } cmd = util.PrependProxyEnv("pkg audit -F -r -f " + vulndbPath) - r = o.ssh(cmd, noSudo) + r = o.exec(cmd, noSudo) if !r.isSuccess(0, 1) { return nil, fmt.Errorf("Failed to SSH: %s", r) } diff --git a/scan/redhat.go b/scan/redhat.go index 8b13ac106c..47d524a18b 100644 --- a/scan/redhat.go +++ b/scan/redhat.go @@ -48,18 +48,18 @@ func newRedhat(c config.ServerInfo) *redhat { func detectRedhat(c config.ServerInfo) (itsMe bool, red osTypeInterface) { red = newRedhat(c) - if r := sshExec(c, "ls /etc/fedora-release", noSudo); r.isSuccess() { + if r := exec(c, "ls /etc/fedora-release", noSudo); r.isSuccess() { red.setDistro("fedora", "unknown") Log.Warn("Fedora not tested yet: %s", r) return true, red } - if r := sshExec(c, "ls /etc/redhat-release", noSudo); r.isSuccess() { + if r := exec(c, "ls /etc/redhat-release", noSudo); r.isSuccess() { // https://www.rackaid.com/blog/how-to-determine-centos-or-red-hat-version/ // e.g. // $ cat /etc/redhat-release // CentOS release 6.5 (Final) - if r := sshExec(c, "cat /etc/redhat-release", noSudo); r.isSuccess() { + if r := exec(c, "cat /etc/redhat-release", noSudo); r.isSuccess() { re := regexp.MustCompile(`(.*) release (\d[\d.]*)`) result := re.FindStringSubmatch(strings.TrimSpace(r.Stdout)) if len(result) != 3 { @@ -79,10 +79,10 @@ func detectRedhat(c config.ServerInfo) (itsMe bool, red osTypeInterface) { return true, red } - if r := sshExec(c, "ls /etc/system-release", noSudo); r.isSuccess() { + if r := exec(c, "ls /etc/system-release", noSudo); r.isSuccess() { family := "amazon" release := "unknown" - if r := sshExec(c, "cat /etc/system-release", noSudo); r.isSuccess() { + if r := exec(c, "cat /etc/system-release", noSudo); r.isSuccess() { fields := strings.Fields(r.Stdout) if len(fields) == 5 { release = fields[4] @@ -97,7 +97,7 @@ func detectRedhat(c config.ServerInfo) (itsMe bool, red osTypeInterface) { } func (o *redhat) checkIfSudoNoPasswd() error { - r := o.ssh("yum --version", o.sudo()) + r := o.exec("yum --version", o.sudo()) if !r.isSuccess() { o.log.Errorf("sudo error on %s", r) return fmt.Errorf("Failed to sudo: %s", r) @@ -128,7 +128,7 @@ func (o *redhat) checkDependencies() error { } cmd := "rpm -q " + name - if r := o.ssh(cmd, noSudo); r.isSuccess() { + if r := o.exec(cmd, noSudo); r.isSuccess() { return nil } o.lackDependencies = []string{name} @@ -142,7 +142,7 @@ func (o *redhat) checkDependencies() error { func (o *redhat) install() error { for _, name := range o.lackDependencies { cmd := util.PrependProxyEnv("yum install -y " + name) - if r := o.ssh(cmd, sudo); !r.isSuccess() { + if r := o.exec(cmd, sudo); !r.isSuccess() { return fmt.Errorf("Failed to SSH: %s", r) } o.log.Infof("Installed: %s", name) @@ -165,7 +165,7 @@ func (o *redhat) checkRequiredPackagesInstalled() error { } cmd := "rpm -q " + packName - if r := o.ssh(cmd, noSudo); !r.isSuccess() { + if r := o.exec(cmd, noSudo); !r.isSuccess() { msg := fmt.Sprintf("%s is not installed", packName) o.log.Errorf(msg) return fmt.Errorf(msg) @@ -194,7 +194,7 @@ func (o *redhat) scanPackages() error { func (o *redhat) scanInstalledPackages() (installedPackages models.PackageInfoList, err error) { cmd := "rpm -qa --queryformat '%{NAME}\t%{VERSION}\t%{RELEASE}\n'" - r := o.ssh(cmd, noSudo) + r := o.exec(cmd, noSudo) if r.isSuccess() { // e.g. // openssl 1.0.1e 30.el6.11 @@ -249,7 +249,7 @@ func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (models.VulnInfos, er cmd = fmt.Sprintf(cmd, "") } - r := o.ssh(util.PrependProxyEnv(cmd), sudo) + r := o.exec(util.PrependProxyEnv(cmd), sudo) if !r.isSuccess(0, 100) { //returns an exit code of 100 if there are available updates. return nil, fmt.Errorf("Failed to SSH: %s", r) @@ -543,7 +543,7 @@ func (o *redhat) getAllChangelog(packInfoList models.PackageInfoList) (stdout st // yum update --changelog doesn't have --color option. command += fmt.Sprintf(" LANGUAGE=en_US.UTF-8 yum %s --changelog update ", yumopts) + packageNames - r := o.ssh(command, sudo) + r := o.exec(command, sudo) if !r.isSuccess(0, 1) { return "", fmt.Errorf( "Failed to get changelog. status: %d, stdout: %s, stderr: %s", @@ -568,7 +568,7 @@ func (o *redhat) scanUnsecurePackagesUsingYumPluginSecurity() (models.VulnInfos, } cmd := "yum --color=never repolist" - r := o.ssh(util.PrependProxyEnv(cmd), o.sudo()) + r := o.exec(util.PrependProxyEnv(cmd), o.sudo()) if !r.isSuccess() { return nil, fmt.Errorf("Failed to SSH: %s", r) } @@ -584,7 +584,7 @@ func (o *redhat) scanUnsecurePackagesUsingYumPluginSecurity() (models.VulnInfos, } else { cmd = "yum --color=never updateinfo list available --security" } - r = o.ssh(util.PrependProxyEnv(cmd), o.sudo()) + r = o.exec(util.PrependProxyEnv(cmd), o.sudo()) if !r.isSuccess() { return nil, fmt.Errorf("Failed to SSH: %s", r) } @@ -593,7 +593,7 @@ func (o *redhat) scanUnsecurePackagesUsingYumPluginSecurity() (models.VulnInfos, // get package name, version, rel to be upgrade. // cmd = "yum check-update --security" cmd = "LANGUAGE=en_US.UTF-8 yum --color=never check-update" - r = o.ssh(util.PrependProxyEnv(cmd), o.sudo()) + r = o.exec(util.PrependProxyEnv(cmd), o.sudo()) if !r.isSuccess(0, 100) { //returns an exit code of 100 if there are available updates. return nil, fmt.Errorf("Failed to SSH: %s", r) @@ -628,7 +628,7 @@ func (o *redhat) scanUnsecurePackagesUsingYumPluginSecurity() (models.VulnInfos, } else { cmd = "yum --color=never updateinfo --security update" } - r = o.ssh(util.PrependProxyEnv(cmd), o.sudo()) + r = o.exec(util.PrependProxyEnv(cmd), o.sudo()) if !r.isSuccess() { return nil, fmt.Errorf("Failed to SSH: %s", r) } diff --git a/scan/serverapi.go b/scan/serverapi.go index abebd80385..98394d5b2d 100644 --- a/scan/serverapi.go +++ b/scan/serverapi.go @@ -105,6 +105,9 @@ func detectOS(c config.ServerInfo) (osType osTypeInterface) { Log.Debugf("FreeBSD. Host: %s:%s", c.Host, c.Port) return } + + //TODO darwin https://github.com/mizzy/specinfra/blob/master/lib/specinfra/helper/detect_os/darwin.rb + osType.setServerInfo(c) osType.setErrs([]error{fmt.Errorf("Unknown OS Type")}) return diff --git a/scan/sshutil.go b/scan/sshutil.go index fe245e158f..268a94c2ee 100644 --- a/scan/sshutil.go +++ b/scan/sshutil.go @@ -25,7 +25,7 @@ import ( "io/ioutil" "net" "os" - "os/exec" + ex "os/exec" "strings" "syscall" "time" @@ -39,7 +39,7 @@ import ( "github.com/future-architect/vuls/util" ) -type sshResult struct { +type execResult struct { Servername string Host string Port string @@ -50,16 +50,13 @@ type sshResult struct { Error error } -func (s sshResult) String() string { +func (s execResult) String() string { return fmt.Sprintf( - "SSHResult: servername: %s, cmd: %s, exitstatus: %d, stdout: %s, stderr: %s, err: %s", + "execResult: servername: %s\n cmd: %s\n exitstatus: %d\n stdout: %s\n stderr: %s\n err: %s", s.Servername, s.Cmd, s.ExitStatus, s.Stdout, s.Stderr, s.Error) } -func (s sshResult) isSuccess(expectedStatusCodes ...int) bool { - if s.Error != nil { - return false - } +func (s execResult) isSuccess(expectedStatusCodes ...int) bool { if len(expectedStatusCodes) == 0 { return s.ExitStatus == 0 } @@ -68,6 +65,9 @@ func (s sshResult) isSuccess(expectedStatusCodes ...int) bool { return true } } + if s.Error != nil { + return false + } return false } @@ -148,8 +148,11 @@ func parallelSSHExec(fn func(osTypeInterface) error, timeoutSec ...int) (errs [] return } -func sshExec(c conf.ServerInfo, cmd string, sudo bool, log ...*logrus.Entry) (result sshResult) { - if conf.Conf.SSHExternal { +func exec(c conf.ServerInfo, cmd string, sudo bool, log ...*logrus.Entry) (result execResult) { + if (c.Port == "" || c.Port == "local") && + (c.Host == "127.0.0.1" || c.Host == "localhost") { + result = localExec(c, cmd, sudo) + } else if conf.Conf.SSHExternal { result = sshExecExternal(c, cmd, sudo) } else { result = sshExecNative(c, cmd, sudo) @@ -160,7 +163,37 @@ func sshExec(c conf.ServerInfo, cmd string, sudo bool, log ...*logrus.Entry) (re return } -func sshExecNative(c conf.ServerInfo, cmd string, sudo bool) (result sshResult) { +func localExec(c conf.ServerInfo, cmdstr string, sudo bool) (result execResult) { + cmdstr = decolateCmd(c, cmdstr, sudo) + var cmd *ex.Cmd + if c.Distro.Family == "FreeBSD" { + cmd = ex.Command("/bin/sh", "-c", cmdstr) + } else { + cmd = ex.Command("/bin/bash", "-c", cmdstr) + } + var stdoutBuf, stderrBuf bytes.Buffer + cmd.Stdout = &stdoutBuf + cmd.Stderr = &stderrBuf + + if err := cmd.Run(); err != nil { + result.Error = err + if exitError, ok := err.(*ex.ExitError); ok { + waitStatus := exitError.Sys().(syscall.WaitStatus) + result.ExitStatus = waitStatus.ExitStatus() + } else { + result.ExitStatus = 999 + } + } else { + result.ExitStatus = 0 + } + + result.Stdout = stdoutBuf.String() + result.Stderr = stderrBuf.String() + result.Cmd = strings.Replace(cmdstr, "\n", "", -1) + return +} + +func sshExecNative(c conf.ServerInfo, cmd string, sudo bool) (result execResult) { result.Servername = c.ServerName result.Host = c.Host result.Port = c.Port @@ -219,8 +252,8 @@ func sshExecNative(c conf.ServerInfo, cmd string, sudo bool) (result sshResult) return } -func sshExecExternal(c conf.ServerInfo, cmd string, sudo bool) (result sshResult) { - sshBinaryPath, err := exec.LookPath("ssh") +func sshExecExternal(c conf.ServerInfo, cmd string, sudo bool) (result execResult) { + sshBinaryPath, err := ex.LookPath("ssh") if err != nil { return sshExecNative(c, cmd, sudo) } @@ -256,13 +289,13 @@ func sshExecExternal(c conf.ServerInfo, cmd string, sudo bool) (result sshResult // cmd = fmt.Sprintf("stty cols 256; set -o pipefail; %s", cmd) args = append(args, cmd) - execCmd := exec.Command(sshBinaryPath, args...) + execCmd := ex.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 e, ok := err.(*ex.ExitError); ok { if s, ok := e.Sys().(syscall.WaitStatus); ok { result.ExitStatus = s.ExitStatus() } else {