Files
vuls/scan/executil.go
Kota Kanbe 44fa2c5800 v0.5.0 (no backwards compatibility) (#478)
* Change config.toml, Auto-generate UUIDs, change structure of optional field

* Detect processes affected by update using yum-ps (#482)

Detect processes affected by update using yum-ps

* Detect processes needs restart using checkrestart on Debian and Ubuntu.

* pass cpename by args when calling FillCveInfo (#513)

* fix new db (#502)

* Include Version,Revision in JSON

* Include hostname in JSON

* Update goval-dictionary's commit hash in Gopkg.lock

* Remove README.ja.md

* update packages (#596)

* fix: change ControlPath to .vuls of SSH option (#618)

* feat: checkrestart for Ubuntu and Debian (#622)

* feat: checkrestart for Ubuntu and Debian

* fix: dependencies check logic of configtest

* feat: need-restarting on RedHat

* refactor: Process.ProcName to Process.Name

* feat: detect a systemd service name of need-restarting-process

* feat: detect a systemd service name of need-restarting-process on Ubuntu

* feat: fill a service name of need-restarting-process, init-system

* Support NVD JSON and CVSS3 of JVN (#605)

* fix: compile errors

* fix: Show CVSS3 on TUI

* fix: test cases

* fix: Avoid null in JSON

* Fix maxCvssScore (#621)

* Fix maxCvssScore

* Update vulninfos.go

* fix(init): remove unnecessary log initialization

* refactor(nvd): use only json feed if exists json data. if not, use xml feed

* fix(scan): make Confidence slice

* feat(CWE): Display CWE name to TUI

* feat(cwe): import CWE defs in Japanese

* feat(cwe): add OWASP Top 10 ranking to CWE if applicable

* feat(scan): add -fast-root mode, implement scan/amazon.go

* refactor(const): change const name JVN to Jvn

* feat(scan): add -fast-root mode, implement scan/centos.go

* refactor(dep): update deps

* fix(amazon): deps check

* feat(scan): add -fast-root mode, implement scan/rhel.go

* feat(scan): add -fast-root mode, implement scan/oracle.go

* fix complile err

* feat(scan): add -fast-root mode, implement scan/debian.go

* fix testcase

* fix(amazon): scan using yum

* fix(configtest): change error message, status when no scannnable servers

* Fix(scan): detect init process logic

* fix(tui): display cvss as table format

* fix(scan): parse a output of reboot-notifier on CentOS6.9

* fix(tui): don't display score, vector when score is zero

* fix(scan): add -offline mode to suse scanner

* fix(scan): fix help message

* feat(scan): enable to define scan mode for each servers in config.toml #510

* refactor(config): chagne cpeNames to cpeURIs

* refactor(config): change dependencyCheckXMLPath to owaspDCXMLPath

* fix(config): containers -> containersIncluded, Excluded, containerType

* feature(report): enable to define cpeURIs for each contaner

* feature(report): enable to specify owasp dc xml path for each container

* fix(discover): fix a template displayed at the end of discover

* feature(report): add ignorePkgsRegexp #665

* feature(report): enable to define ignoreCves for each container #666

* fix(report): Displayed nothing in TUI detail area when CweID is nil

* Gopkg.toml diet

* feat(server): support server mode (#678)

* feat(server): support server mode

* Lock go version

* Use the latest kernel release among the installed release when the running kernel release is unknown

* Add TestViaHTTP

* Set logger to go-cve-dictionary client

* Add -to-localfile

* Add -to-http option to report

* Load -to-http conf from config.toml

* Support gost (#676)

* feat(gost): Support RedHat API

* feat(gost): Support Debian Security Tracker

* feat(db): display error msg when SQLite3 is locked at the beginning of reporting.

* feat(gost): TUI

* Only use RedHat information of installed packages

* feat(tui): show mitigation on TUI

* feat(gost): support redis backend

* fix test case

* fix nil pointer when db is nil

* fix(gost): detect vulns of src packages for Debian

* feat(gost): implement redis backend for gost redhat api

* feat(report): display fixState of unfixed pkgs

* fix(report): display distincted cweIDs

* feat(slack): display gost info

* feat(slack): display mitigation

* feat(report): display available patch state as fixed/total

* fix(tui): display - if source of reference is empty

* update deps

* fix(report): key in ScanResult JSON be lowerCamelcase.

* some keys to lower camel

* fix(configtest): dep check logic of yum-plugin-ps

* fix(tui): format

* feat(report): add -format-list option

* fix(report): -format-full-text

* fix(report): report -format-full-text

* fix(report): display v3 score detected by gost

* fix(scan): scan in fast mode if not defined in config.toml

* fix(gost): fetch RedHat data for fixed CVEs

* feat(report): show number of cves detected in each database

* fix(report): show new version as `Unknown` in offline and fast scan mode

* fix(report): fix num of upadtable and fixed

* fix(report): set `Not fixed yet` if packageStatus is empty

* refact(gost): make convertToModel public

* fix(test): fix test case

* update deps

* fix(report): include gost score in MaxCvssScore

* [WIP] feat(config): enable to set options in config.toml instead of cmd opt (#690)

* feat(config): enable to set options in config.toml instead of cmd opt

* fix(config): change Conf.Report.Slack to Conf.Slack

* fix(discover): change tempalte

* fix(report): fix config.toml auto-generate with -uuid

* Add endpoint for health check and change endpoint

* refact(cmd): refactor flag set

* fix(report): enable to specify opts with cmd arg and env value

* fix(scan): enable to parse the release version of amazon linux 2

* add(report) add -to-saas option (#695)

* add(report) add -to-saas option

* ignore other writer if -to-saas

* fix(saas) fix bug

* fix(scan): need-restarting needs internet connection

* fix(scan,configtest): check scan mode

* refactor(scan): change func name

* fix(suse): support offline mode, bug fix on AWS, zypper --no-color

* fix(tui): fix nil pointer when no vulns in tui

* feat(report): enable to define CPE FS format in config.toml

* fix(vet): fix warnings of go vet

* fix(travis): go version to 1.11

* update deps
2018-08-27 13:51:09 +09:00

505 lines
13 KiB
Go

/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Corporation , Japan.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package scan
import (
"bytes"
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"net"
"os"
ex "os/exec"
"path/filepath"
"strings"
"syscall"
"time"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"github.com/cenkalti/backoff"
conf "github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/util"
homedir "github.com/mitchellh/go-homedir"
"github.com/sirupsen/logrus"
)
type execResult struct {
Servername string
Container conf.Container
Host string
Port string
Cmd string
Stdout string
Stderr string
ExitStatus int
Error error
}
func (s execResult) String() string {
sname := ""
if s.Container.ContainerID == "" {
sname = s.Servername
} else {
sname = s.Container.Name + "@" + s.Servername
}
return fmt.Sprintf(
"execResult: servername: %s\n cmd: %s\n exitstatus: %d\n stdout: %s\n stderr: %s\n err: %s",
sname, s.Cmd, s.ExitStatus, s.Stdout, s.Stderr, s.Error)
}
func (s execResult) isSuccess(expectedStatusCodes ...int) bool {
if len(expectedStatusCodes) == 0 {
return s.ExitStatus == 0
}
for _, code := range expectedStatusCodes {
if code == s.ExitStatus {
return true
}
}
if s.Error != nil {
return false
}
return false
}
// sudo is Const value for sudo mode
const sudo = true
// noSudo is Const value for normal user mode
const noSudo = false
// Issue commands to the target servers in parallel via SSH or local execution. If execution fails, the server will be excluded from the target server list(servers) and added to the error server list(errServers).
func parallelExec(fn func(osTypeInterface) error, timeoutSec ...int) {
resChan := make(chan osTypeInterface, len(servers))
defer close(resChan)
for _, s := range servers {
go func(s osTypeInterface) {
defer func() {
if p := recover(); p != nil {
util.Log.Debugf("Panic: %s on %s",
p, s.getServerInfo().GetServerName())
}
}()
if err := fn(s); err != nil {
s.setErrs([]error{err})
resChan <- s
} else {
resChan <- s
}
}(s)
}
var timeout int
if len(timeoutSec) == 0 {
timeout = 10 * 60
} else {
timeout = timeoutSec[0]
}
var successes []osTypeInterface
isTimedout := false
for i := 0; i < len(servers); i++ {
select {
case s := <-resChan:
if len(s.getErrs()) == 0 {
successes = append(successes, s)
} else {
util.Log.Errorf("Error: %s, err: %s",
s.getServerInfo().GetServerName(), s.getErrs())
errServers = append(errServers, s)
}
case <-time.After(time.Duration(timeout) * time.Second):
isTimedout = true
}
}
if isTimedout {
// set timed out error and append to errServers
for _, s := range servers {
name := s.getServerInfo().GetServerName()
found := false
for _, ss := range successes {
if name == ss.getServerInfo().GetServerName() {
found = true
break
}
}
if !found {
msg := fmt.Sprintf("Timed out: %s",
s.getServerInfo().GetServerName())
util.Log.Errorf(msg)
s.setErrs([]error{fmt.Errorf(msg)})
errServers = append(errServers, s)
}
}
}
servers = successes
return
}
func exec(c conf.ServerInfo, cmd string, sudo bool, log ...*logrus.Entry) (result execResult) {
logger := getSSHLogger(log...)
logger.Debugf("Executing... %s", strings.Replace(cmd, "\n", "", -1))
if c.Port == "local" &&
(c.Host == "127.0.0.1" || c.Host == "localhost") {
result = localExec(c, cmd, sudo)
} else if conf.Conf.SSHNative {
result = sshExecNative(c, cmd, sudo)
} else {
result = sshExecExternal(c, cmd, sudo)
}
logger.Debug(result)
return
}
func localExec(c conf.ServerInfo, cmdstr string, sudo bool) (result execResult) {
cmdstr = decorateCmd(c, cmdstr, sudo)
var cmd *ex.Cmd
switch c.Distro.Family {
// case conf.FreeBSD, conf.Alpine, conf.Debian:
// cmd = ex.Command("/bin/sh", "-c", cmdstr)
default:
cmd = ex.Command("/bin/sh", "-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.Container = c.Container
result.Host = c.Host
result.Port = c.Port
var client *ssh.Client
var err error
if client, err = sshConnect(c); err != nil {
result.Error = err
result.ExitStatus = 999
return
}
defer client.Close()
var session *ssh.Session
if session, err = client.NewSession(); err != nil {
result.Error = fmt.Errorf(
"Failed to create a new session. servername: %s, err: %s",
c.ServerName, err)
result.ExitStatus = 999
return
}
defer session.Close()
// http://blog.ralch.com/tutorial/golang-ssh-connection/
modes := ssh.TerminalModes{
ssh.ECHO: 0, // disable echoing
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
}
if err = session.RequestPty("xterm", 400, 1000, modes); err != nil {
result.Error = fmt.Errorf(
"Failed to request for pseudo terminal. servername: %s, err: %s",
c.ServerName, err)
result.ExitStatus = 999
return
}
var stdoutBuf, stderrBuf bytes.Buffer
session.Stdout = &stdoutBuf
session.Stderr = &stderrBuf
cmd = decorateCmd(c, cmd, sudo)
if err := session.Run(cmd); err != nil {
if exitErr, ok := err.(*ssh.ExitError); ok {
result.ExitStatus = exitErr.ExitStatus()
} else {
result.ExitStatus = 999
}
} else {
result.ExitStatus = 0
}
result.Stdout = stdoutBuf.String()
result.Stderr = stderrBuf.String()
result.Cmd = strings.Replace(cmd, "\n", "", -1)
return
}
func sshExecExternal(c conf.ServerInfo, cmd string, sudo bool) (result execResult) {
sshBinaryPath, err := ex.LookPath("ssh")
if err != nil {
return sshExecNative(c, cmd, sudo)
}
defaultSSHArgs := []string{"-tt"}
if !conf.Conf.SSHConfig {
home, err := homedir.Dir()
if err != nil {
msg := fmt.Sprintf("Failed to get HOME directory: %s", err)
result.Stderr = msg
result.ExitStatus = 997
return
}
controlPath := filepath.Join(home, ".vuls", `controlmaster-%r-`+c.ServerName+`.%p`)
defaultSSHArgs = append(defaultSSHArgs,
"-o", "StrictHostKeyChecking=yes",
"-o", "LogLevel=quiet",
"-o", "ConnectionAttempts=3",
"-o", "ConnectTimeout=10",
"-o", "ControlMaster=auto",
"-o", fmt.Sprintf("ControlPath=%s", controlPath),
"-o", "Controlpersist=10m",
)
}
if conf.Conf.Vvv {
defaultSSHArgs = append(defaultSSHArgs, "-vvv")
}
args := append(defaultSSHArgs, fmt.Sprintf("%s@%s", c.User, c.Host))
args = append(args, "-p", c.Port)
if 0 < len(c.KeyPath) {
args = append(args, "-i", c.KeyPath)
args = append(args, "-o", "PasswordAuthentication=no")
}
cmd = decorateCmd(c, cmd, sudo)
cmd = fmt.Sprintf("stty cols 1000; %s", cmd)
args = append(args, cmd)
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.(*ex.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.Servername = c.ServerName
result.Container = c.Container
result.Host = c.Host
result.Port = c.Port
result.Cmd = fmt.Sprintf("%s %s", sshBinaryPath, strings.Join(args, " "))
return
}
func getSSHLogger(log ...*logrus.Entry) *logrus.Entry {
if len(log) == 0 {
return util.NewCustomLogger(conf.ServerInfo{})
}
return log[0]
}
func dockerShell(family string) string {
switch family {
// case conf.Alpine, conf.Debian:
// return "/bin/sh"
default:
// return "/bin/bash"
return "/bin/sh"
}
}
func decorateCmd(c conf.ServerInfo, cmd string, sudo bool) string {
if sudo && c.User != "root" && !c.IsContainer() {
cmd = fmt.Sprintf("sudo -S %s", cmd)
}
// If you are using pipe and you want to detect preprocessing errors, remove comment out
// switch c.Distro.Family {
// case "FreeBSD", "ubuntu", "debian", "raspbian":
// default:
// // 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.ContainerType {
case "", "docker":
cmd = fmt.Sprintf(`docker exec --user 0 %s %s -c '%s'`,
c.Container.ContainerID, dockerShell(c.Distro.Family), cmd)
case "lxd":
// If the user belong to the "lxd" group, root privilege is not required.
cmd = fmt.Sprintf(`lxc exec %s -- %s -c '%s'`,
c.Container.Name, dockerShell(c.Distro.Family), cmd)
case "lxc":
cmd = fmt.Sprintf(`lxc-attach -n %s 2>/dev/null -- %s -c '%s'`,
c.Container.Name, dockerShell(c.Distro.Family), cmd)
// LXC required root privilege
if c.User != "root" {
cmd = fmt.Sprintf("sudo -S %s", cmd)
}
}
}
// cmd = fmt.Sprintf("set -x; %s", cmd)
return cmd
}
func getAgentAuth() (auth ssh.AuthMethod, ok bool) {
if sock := os.Getenv("SSH_AUTH_SOCK"); 0 < len(sock) {
if agconn, err := net.Dial("unix", sock); err == nil {
ag := agent.NewClient(agconn)
auth = ssh.PublicKeysCallback(ag.Signers)
ok = true
}
}
return
}
func tryAgentConnect(c conf.ServerInfo) *ssh.Client {
if auth, ok := getAgentAuth(); ok {
config := &ssh.ClientConfig{
User: c.User,
Auth: []ssh.AuthMethod{auth},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
client, _ := ssh.Dial("tcp", c.Host+":"+c.Port, config)
return client
}
return nil
}
func sshConnect(c conf.ServerInfo) (client *ssh.Client, err error) {
if client = tryAgentConnect(c); client != nil {
return client, nil
}
var auths = []ssh.AuthMethod{}
if auths, err = addKeyAuth(auths, c.KeyPath, c.KeyPassword); err != nil {
return nil, err
}
// http://blog.ralch.com/tutorial/golang-ssh-connection/
config := &ssh.ClientConfig{
User: c.User,
Auth: auths,
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
notifyFunc := func(e error, t time.Duration) {
logger := getSSHLogger()
logger.Debugf("Failed to Dial to %s, err: %s, Retrying in %s...",
c.ServerName, e, t)
}
err = backoff.RetryNotify(func() error {
if client, err = ssh.Dial("tcp", c.Host+":"+c.Port, config); err != nil {
return err
}
return nil
}, backoff.NewExponentialBackOff(), notifyFunc)
return
}
// https://github.com/rapidloop/rtop/blob/ba5b35e964135d50e0babedf0bd69b2fcb5dbcb4/src/sshhelper.go#L100
func addKeyAuth(auths []ssh.AuthMethod, keypath string, keypassword string) ([]ssh.AuthMethod, error) {
if len(keypath) == 0 {
return auths, nil
}
// read the file
pemBytes, err := ioutil.ReadFile(keypath)
if err != nil {
return auths, err
}
// get first pem block
block, _ := pem.Decode(pemBytes)
if block == nil {
return auths, fmt.Errorf("no key found in %s", keypath)
}
// handle plain and encrypted keyfiles
if x509.IsEncryptedPEMBlock(block) {
block.Bytes, err = x509.DecryptPEMBlock(block, []byte(keypassword))
if err != nil {
return auths, err
}
key, err := parsePemBlock(block)
if err != nil {
return auths, err
}
signer, err := ssh.NewSignerFromKey(key)
if err != nil {
return auths, err
}
return append(auths, ssh.PublicKeys(signer)), nil
}
signer, err := ssh.ParsePrivateKey(pemBytes)
if err != nil {
return auths, err
}
return append(auths, ssh.PublicKeys(signer)), nil
}
// ref golang.org/x/crypto/ssh/keys.go#ParseRawPrivateKey.
func parsePemBlock(block *pem.Block) (interface{}, error) {
switch block.Type {
case "RSA PRIVATE KEY":
return x509.ParsePKCS1PrivateKey(block.Bytes)
case "EC PRIVATE KEY":
return x509.ParseECPrivateKey(block.Bytes)
case "DSA PRIVATE KEY":
return ssh.ParseDSAPrivateKey(block.Bytes)
default:
return nil, fmt.Errorf("Unsupported key type %q", block.Type)
}
}