1014 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			1014 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
/* Vuls - Vulnerability Scanner
 | 
						|
Copyright (C) 2016  Future Architect, Inc. 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 (
 | 
						|
	"bufio"
 | 
						|
	"fmt"
 | 
						|
	"regexp"
 | 
						|
	"strings"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/future-architect/vuls/config"
 | 
						|
	"github.com/future-architect/vuls/models"
 | 
						|
	"github.com/future-architect/vuls/util"
 | 
						|
 | 
						|
	ver "github.com/knqyf263/go-rpm-version"
 | 
						|
)
 | 
						|
 | 
						|
// inherit OsTypeInterface
 | 
						|
type redhat struct {
 | 
						|
	base
 | 
						|
}
 | 
						|
 | 
						|
// NewRedhat is constructor
 | 
						|
func newRedhat(c config.ServerInfo) *redhat {
 | 
						|
	r := &redhat{
 | 
						|
		base: base{
 | 
						|
			osPackages: osPackages{
 | 
						|
				Packages:  models.Packages{},
 | 
						|
				VulnInfos: models.VulnInfos{},
 | 
						|
			},
 | 
						|
		},
 | 
						|
	}
 | 
						|
	r.log = util.NewCustomLogger(c)
 | 
						|
	r.setServerInfo(c)
 | 
						|
	return r
 | 
						|
}
 | 
						|
 | 
						|
// https://github.com/serverspec/specinfra/blob/master/lib/specinfra/helper/detect_os/redhat.rb
 | 
						|
func detectRedhat(c config.ServerInfo) (itsMe bool, red osTypeInterface) {
 | 
						|
	red = newRedhat(c)
 | 
						|
 | 
						|
	if r := exec(c, "ls /etc/fedora-release", noSudo); r.isSuccess() {
 | 
						|
		red.setDistro(config.Fedora, "unknown")
 | 
						|
		util.Log.Warn("Fedora not tested yet: %s", r)
 | 
						|
		return true, red
 | 
						|
	}
 | 
						|
 | 
						|
	if r := exec(c, "ls /etc/oracle-release", noSudo); r.isSuccess() {
 | 
						|
		// Need to discover Oracle Linux first, because it provides an
 | 
						|
		// /etc/redhat-release that matches the upstream distribution
 | 
						|
		if r := exec(c, "cat /etc/oracle-release", noSudo); r.isSuccess() {
 | 
						|
			re := regexp.MustCompile(`(.*) release (\d[\d.]*)`)
 | 
						|
			result := re.FindStringSubmatch(strings.TrimSpace(r.Stdout))
 | 
						|
			if len(result) != 3 {
 | 
						|
				util.Log.Warn("Failed to parse Oracle Linux version: %s", r)
 | 
						|
				return true, red
 | 
						|
			}
 | 
						|
 | 
						|
			release := result[2]
 | 
						|
			red.setDistro(config.Oracle, release)
 | 
						|
			return true, red
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	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 := 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 {
 | 
						|
				util.Log.Warn("Failed to parse RedHat/CentOS version: %s", r)
 | 
						|
				return true, red
 | 
						|
			}
 | 
						|
 | 
						|
			release := result[2]
 | 
						|
			switch strings.ToLower(result[1]) {
 | 
						|
			case "centos", "centos linux":
 | 
						|
				red.setDistro(config.CentOS, release)
 | 
						|
			default:
 | 
						|
				red.setDistro(config.RedHat, release)
 | 
						|
			}
 | 
						|
			return true, red
 | 
						|
		}
 | 
						|
		return true, red
 | 
						|
	}
 | 
						|
 | 
						|
	if r := exec(c, "ls /etc/system-release", noSudo); r.isSuccess() {
 | 
						|
		family := config.Amazon
 | 
						|
		release := "unknown"
 | 
						|
		if r := exec(c, "cat /etc/system-release", noSudo); r.isSuccess() {
 | 
						|
			fields := strings.Fields(r.Stdout)
 | 
						|
			if len(fields) == 5 {
 | 
						|
				release = fields[4]
 | 
						|
			}
 | 
						|
		}
 | 
						|
		red.setDistro(family, release)
 | 
						|
		return true, red
 | 
						|
	}
 | 
						|
 | 
						|
	util.Log.Debugf("Not RedHat like Linux. servername: %s", c.ServerName)
 | 
						|
	return false, red
 | 
						|
}
 | 
						|
 | 
						|
func (o *redhat) checkIfSudoNoPasswd() error {
 | 
						|
	if !config.Conf.Deep || !o.sudo() {
 | 
						|
		o.log.Infof("sudo ... No need")
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	type cmd struct {
 | 
						|
		cmd                 string
 | 
						|
		expectedStatusCodes []int
 | 
						|
	}
 | 
						|
	var cmds []cmd
 | 
						|
	var zero = []int{0}
 | 
						|
 | 
						|
	switch o.Distro.Family {
 | 
						|
	case config.RedHat, config.Oracle:
 | 
						|
		majorVersion, err := o.Distro.MajorVersion()
 | 
						|
		if err != nil {
 | 
						|
			return fmt.Errorf("Not implemented yet: %s, err: %s", o.Distro, err)
 | 
						|
		}
 | 
						|
 | 
						|
		if majorVersion < 6 {
 | 
						|
			cmds = []cmd{
 | 
						|
				{"yum --color=never repolist", zero},
 | 
						|
				{"yum --color=never list-security --security", zero},
 | 
						|
				{"yum --color=never info-security", zero},
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			cmds = []cmd{
 | 
						|
				{"yum --color=never repolist", zero},
 | 
						|
				{"yum --color=never --security updateinfo list updates", zero},
 | 
						|
				{"yum --color=never --security updateinfo updates", zero},
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	for _, c := range cmds {
 | 
						|
		cmd := util.PrependProxyEnv(c.cmd)
 | 
						|
		o.log.Infof("Checking... sudo %s", cmd)
 | 
						|
		r := o.exec(util.PrependProxyEnv(cmd), o.sudo())
 | 
						|
		if !r.isSuccess(c.expectedStatusCodes...) {
 | 
						|
			o.log.Errorf("Check sudo or proxy settings: %s", r)
 | 
						|
			return fmt.Errorf("Failed to sudo: %s", r)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	o.log.Infof("Sudo... Pass")
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// - Fast scan mode
 | 
						|
//    No additional dependencies needed
 | 
						|
//
 | 
						|
// - Deep scan mode
 | 
						|
//    CentOS 6, 7 	... yum-utils
 | 
						|
//    RHEL 5     	... yum-security
 | 
						|
//    RHEL 6, 7     ... yum-utils
 | 
						|
//    Amazon 		... yum-utils
 | 
						|
func (o *redhat) checkDependencies() error {
 | 
						|
	if !config.Conf.Deep {
 | 
						|
		o.log.Infof("Dependencies... No need")
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	majorVersion, err := o.Distro.MajorVersion()
 | 
						|
	if err != nil {
 | 
						|
		msg := fmt.Sprintf("Not implemented yet: %s, err: %s", o.Distro, err)
 | 
						|
		o.log.Errorf(msg)
 | 
						|
		return fmt.Errorf(msg)
 | 
						|
	}
 | 
						|
 | 
						|
	if o.Distro.Family == config.CentOS {
 | 
						|
		if majorVersion < 6 {
 | 
						|
			msg := fmt.Sprintf("CentOS %s is not supported", o.Distro.Release)
 | 
						|
			o.log.Errorf(msg)
 | 
						|
			return fmt.Errorf(msg)
 | 
						|
		}
 | 
						|
 | 
						|
		// --assumeno option of yum is needed.
 | 
						|
		cmd := "yum -h | grep assumeno"
 | 
						|
		if r := o.exec(cmd, noSudo); !r.isSuccess() {
 | 
						|
			msg := fmt.Sprintf("Installed yum is old. Please update yum and then retry")
 | 
						|
			o.log.Errorf(msg)
 | 
						|
			return fmt.Errorf(msg)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	var packNames []string
 | 
						|
	switch o.Distro.Family {
 | 
						|
	case config.CentOS, config.Amazon:
 | 
						|
		packNames = []string{"yum-utils, yum-plugin-changelog"}
 | 
						|
	case config.RedHat, config.Oracle:
 | 
						|
		if majorVersion < 6 {
 | 
						|
			packNames = []string{"yum-utils", "yum-security", "yum-changelog"}
 | 
						|
		} else {
 | 
						|
			packNames = []string{"yum-plugin-changelog"}
 | 
						|
		}
 | 
						|
	default:
 | 
						|
		return fmt.Errorf("Not implemented yet: %s", o.Distro)
 | 
						|
	}
 | 
						|
 | 
						|
	for _, name := range packNames {
 | 
						|
		cmd := "rpm -q " + name
 | 
						|
		if r := o.exec(cmd, noSudo); !r.isSuccess() {
 | 
						|
			msg := fmt.Sprintf("%s is not installed", name)
 | 
						|
			o.log.Errorf(msg)
 | 
						|
			return fmt.Errorf(msg)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	o.log.Infof("Dependencies ... Pass")
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (o *redhat) scanPackages() error {
 | 
						|
	installed, err := o.scanInstalledPackages()
 | 
						|
	if err != nil {
 | 
						|
		o.log.Errorf("Failed to scan installed packages")
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	updatable, err := o.scanUpdatablePackages()
 | 
						|
	if err != nil {
 | 
						|
		o.log.Errorf("Failed to scan installed packages")
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	installed.MergeNewVersion(updatable)
 | 
						|
	o.setPackages(installed)
 | 
						|
 | 
						|
	if !config.Conf.Deep && o.Distro.Family != config.Amazon {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	var vinfos models.VulnInfos
 | 
						|
	if vinfos, err = o.scanUnsecurePackages(updatable); err != nil {
 | 
						|
		o.log.Errorf("Failed to scan vulnerable packages")
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	o.setVulnInfos(vinfos)
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (o *redhat) scanInstalledPackages() (models.Packages, error) {
 | 
						|
	installed := models.Packages{}
 | 
						|
	var cmd string
 | 
						|
	majorVersion, _ := o.Distro.MajorVersion()
 | 
						|
	if majorVersion < 6 {
 | 
						|
		cmd = "rpm -qa --queryformat '%{NAME} %{EPOCH} %{VERSION} %{RELEASE} %{ARCH}\n'"
 | 
						|
	} else {
 | 
						|
		cmd = "rpm -qa --queryformat '%{NAME} %{EPOCHNUM} %{VERSION} %{RELEASE} %{ARCH}\n'"
 | 
						|
	}
 | 
						|
	r := o.exec(cmd, noSudo)
 | 
						|
	if r.isSuccess() {
 | 
						|
		// openssl 0 1.0.1e	30.el6.11 x86_64
 | 
						|
		lines := strings.Split(r.Stdout, "\n")
 | 
						|
		for _, line := range lines {
 | 
						|
			if trimed := strings.TrimSpace(line); len(trimed) != 0 {
 | 
						|
				pack, err := o.parseInstalledPackagesLine(line)
 | 
						|
				if err != nil {
 | 
						|
					return nil, err
 | 
						|
				}
 | 
						|
				installed[pack.Name] = pack
 | 
						|
			}
 | 
						|
		}
 | 
						|
		return installed, nil
 | 
						|
	}
 | 
						|
 | 
						|
	return nil, fmt.Errorf("Scan packages failed. status: %d, stdout: %s, stderr: %s",
 | 
						|
		r.ExitStatus, r.Stdout, r.Stderr)
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
func (o *redhat) parseInstalledPackagesLine(line string) (models.Package, error) {
 | 
						|
	fields := strings.Fields(line)
 | 
						|
	if len(fields) != 5 {
 | 
						|
		return models.Package{},
 | 
						|
			fmt.Errorf("Failed to parse package line: %s", line)
 | 
						|
	}
 | 
						|
	ver := ""
 | 
						|
	epoch := fields[1]
 | 
						|
	if epoch == "0" || epoch == "(none)" {
 | 
						|
		ver = fields[2]
 | 
						|
	} else {
 | 
						|
		ver = fmt.Sprintf("%s:%s", epoch, fields[2])
 | 
						|
	}
 | 
						|
 | 
						|
	return models.Package{
 | 
						|
		Name:    fields[0],
 | 
						|
		Version: ver,
 | 
						|
		Release: fields[3],
 | 
						|
		Arch:    fields[4],
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
func (o *redhat) scanUpdatablePackages() (models.Packages, error) {
 | 
						|
	cmd := "repoquery --all --pkgnarrow=updates --qf='%{NAME} %{EPOCH} %{VERSION} %{RELEASE} %{REPO}'"
 | 
						|
	for _, repo := range o.getServerInfo().Enablerepo {
 | 
						|
		cmd += " --enablerepo=" + repo
 | 
						|
	}
 | 
						|
 | 
						|
	r := o.exec(util.PrependProxyEnv(cmd), o.sudo())
 | 
						|
	if !r.isSuccess() {
 | 
						|
		return nil, fmt.Errorf("Failed to SSH: %s", r)
 | 
						|
	}
 | 
						|
 | 
						|
	// Collect Updateble packages, installed, candidate version and repository.
 | 
						|
	return o.parseUpdatablePacksLines(r.Stdout)
 | 
						|
}
 | 
						|
 | 
						|
// parseUpdatablePacksLines parse the stdout of repoquery to get package name, candidate version
 | 
						|
func (o *redhat) parseUpdatablePacksLines(stdout string) (models.Packages, error) {
 | 
						|
	updatable := models.Packages{}
 | 
						|
	lines := strings.Split(stdout, "\n")
 | 
						|
	for _, line := range lines {
 | 
						|
		// TODO remove
 | 
						|
		// if strings.HasPrefix(line, "Obsoleting") ||
 | 
						|
		// strings.HasPrefix(line, "Security:") {
 | 
						|
		// // see https://github.com/future-architect/vuls/issues/165
 | 
						|
		// continue
 | 
						|
		// }
 | 
						|
		if len(strings.TrimSpace(line)) == 0 {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		pack, err := o.parseUpdatablePacksLine(line)
 | 
						|
		if err != nil {
 | 
						|
			return updatable, err
 | 
						|
		}
 | 
						|
		updatable[pack.Name] = pack
 | 
						|
	}
 | 
						|
	return updatable, nil
 | 
						|
}
 | 
						|
 | 
						|
func (o *redhat) parseUpdatablePacksLine(line string) (models.Package, error) {
 | 
						|
	fields := strings.Fields(line)
 | 
						|
	if len(fields) < 5 {
 | 
						|
		return models.Package{}, fmt.Errorf("Unknown format: %s, fields: %s", line, fields)
 | 
						|
	}
 | 
						|
 | 
						|
	ver := ""
 | 
						|
	epoch := fields[1]
 | 
						|
	if epoch == "0" {
 | 
						|
		ver = fields[2]
 | 
						|
	} else {
 | 
						|
		ver = fmt.Sprintf("%s:%s", epoch, fields[2])
 | 
						|
	}
 | 
						|
 | 
						|
	repos := strings.Join(fields[4:len(fields)], " ")
 | 
						|
 | 
						|
	p := models.Package{
 | 
						|
		Name:       fields[0],
 | 
						|
		NewVersion: ver,
 | 
						|
		NewRelease: fields[3],
 | 
						|
		Repository: repos,
 | 
						|
	}
 | 
						|
	return p, nil
 | 
						|
}
 | 
						|
 | 
						|
func (o *redhat) scanUnsecurePackages(updatable models.Packages) (models.VulnInfos, error) {
 | 
						|
	if config.Conf.Deep {
 | 
						|
		//TODO Cache changelogs to bolt
 | 
						|
		if err := o.fillChangelogs(updatable); err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if o.Distro.Family != config.CentOS {
 | 
						|
		// Amazon, RHEL, Oracle Linux has yum updateinfo as default
 | 
						|
		// yum updateinfo can collenct vendor advisory information.
 | 
						|
		return o.scanCveIDsByCommands(updatable)
 | 
						|
	}
 | 
						|
 | 
						|
	// Parse chnagelog because CentOS does not have security channel...
 | 
						|
	return o.scanCveIDsInChangelog(updatable)
 | 
						|
}
 | 
						|
 | 
						|
func (o *redhat) fillChangelogs(updatables models.Packages) error {
 | 
						|
	names := []string{}
 | 
						|
	for name := range updatables {
 | 
						|
		names = append(names, name)
 | 
						|
	}
 | 
						|
 | 
						|
	if err := o.fillDiffChangelogs(names); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	emptyChangelogPackNames := []string{}
 | 
						|
	for _, pack := range o.Packages {
 | 
						|
		if pack.NewVersion != "" && pack.Changelog.Contents == "" {
 | 
						|
			emptyChangelogPackNames = append(emptyChangelogPackNames, pack.Name)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	i := 0
 | 
						|
	for _, name := range emptyChangelogPackNames {
 | 
						|
		i++
 | 
						|
		o.log.Infof("(%d/%d) Fetched Changelogs %s", i, len(emptyChangelogPackNames), name)
 | 
						|
		if err := o.fillDiffChangelogs([]string{name}); err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (o *redhat) getAvailableChangelogs(packNames []string) (map[string]string, error) {
 | 
						|
	yumopts := ""
 | 
						|
	if 0 < len(o.getServerInfo().Enablerepo) {
 | 
						|
		yumopts = " --enablerepo=" + strings.Join(o.getServerInfo().Enablerepo, ",")
 | 
						|
	}
 | 
						|
	if config.Conf.SkipBroken {
 | 
						|
		yumopts += " --skip-broken"
 | 
						|
	}
 | 
						|
	cmd := `yum --color=never %s changelog all %s | grep -A 10000 '==================== Available Packages ===================='`
 | 
						|
	cmd = fmt.Sprintf(cmd, yumopts, strings.Join(packNames, " "))
 | 
						|
 | 
						|
	r := o.exec(util.PrependProxyEnv(cmd), noSudo)
 | 
						|
	if !r.isSuccess(0, 1) {
 | 
						|
		return nil, fmt.Errorf("Failed to SSH: %s", r)
 | 
						|
	}
 | 
						|
 | 
						|
	return o.divideChangelogsIntoEachPackages(r.Stdout), nil
 | 
						|
}
 | 
						|
 | 
						|
// Divide available change logs of all updatable packages into each package's changelog
 | 
						|
func (o *redhat) divideChangelogsIntoEachPackages(stdout string) map[string]string {
 | 
						|
	changelogs := make(map[string]string)
 | 
						|
	scanner := bufio.NewScanner(strings.NewReader(stdout))
 | 
						|
 | 
						|
	crlf, newBlock := false, true
 | 
						|
	packNameVer, contents := "", []string{}
 | 
						|
	for scanner.Scan() {
 | 
						|
		line := scanner.Text()
 | 
						|
		if strings.HasPrefix(line, "==================== Available Packages ====================") {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		if newBlock {
 | 
						|
			left := strings.Fields(line)[0]
 | 
						|
			// ss := strings.Split(left, ".")
 | 
						|
			// packNameVer = strings.Join(ss[0:len(ss)-1], ".")
 | 
						|
			packNameVer = left
 | 
						|
			newBlock = false
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		if len(strings.TrimSpace(line)) == 0 {
 | 
						|
			if crlf {
 | 
						|
				changelogs[packNameVer] = strings.Join(contents, "\n")
 | 
						|
				packNameVer = ""
 | 
						|
				contents = []string{}
 | 
						|
				newBlock = true
 | 
						|
				crlf = false
 | 
						|
			} else {
 | 
						|
				contents = append(contents, line)
 | 
						|
				crlf = true
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			contents = append(contents, line)
 | 
						|
			crlf = false
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if 0 < len(contents) {
 | 
						|
		changelogs[packNameVer] = strings.Join(contents, "\n")
 | 
						|
	}
 | 
						|
	return changelogs
 | 
						|
}
 | 
						|
 | 
						|
func (o *redhat) fillDiffChangelogs(packNames []string) error {
 | 
						|
	changelogs, err := o.getAvailableChangelogs(packNames)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	for s := range changelogs {
 | 
						|
		// name, pack, found := o.Packages.FindOne(func(p models.Package) bool {
 | 
						|
		name, pack, found := o.Packages.FindOne(func(p models.Package) bool {
 | 
						|
			var epochNameVerRel string
 | 
						|
			if index := strings.Index(p.NewVersion, ":"); 0 < index {
 | 
						|
				epoch := p.NewVersion[0:index]
 | 
						|
				ver := p.NewVersion[index+1 : len(p.NewVersion)]
 | 
						|
				epochNameVerRel = fmt.Sprintf("%s:%s-%s",
 | 
						|
					epoch, p.Name, ver)
 | 
						|
			} else {
 | 
						|
				epochNameVerRel = fmt.Sprintf("%s-%s",
 | 
						|
					p.Name, p.NewVersion)
 | 
						|
			}
 | 
						|
			return strings.HasPrefix(s, epochNameVerRel)
 | 
						|
		})
 | 
						|
 | 
						|
		if found {
 | 
						|
			diff, err := o.getDiffChangelog(pack, changelogs[s])
 | 
						|
			detectionMethod := models.ChangelogExactMatchStr
 | 
						|
 | 
						|
			if err != nil {
 | 
						|
				o.log.Debug(err)
 | 
						|
				// Try without epoch
 | 
						|
				if index := strings.Index(pack.Version, ":"); 0 < index {
 | 
						|
					pack.Version = pack.Version[index+1 : len(pack.Version)]
 | 
						|
					o.log.Debug("Try without epoch", pack)
 | 
						|
					diff, err = o.getDiffChangelog(pack, changelogs[s])
 | 
						|
					if err != nil {
 | 
						|
						o.log.Debugf("Failed to find the version in changelog: %s-%s-%s",
 | 
						|
							pack.Name, pack.Version, pack.Release)
 | 
						|
						detectionMethod = models.FailedToFindVersionInChangelog
 | 
						|
					} else {
 | 
						|
						o.log.Debugf("Found the version in changelog without epoch: %s-%s-%s",
 | 
						|
							pack.Name, pack.Version, pack.Release)
 | 
						|
						detectionMethod = models.ChangelogLenientMatchStr
 | 
						|
					}
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			pack = o.Packages[name]
 | 
						|
			pack.Changelog = models.Changelog{
 | 
						|
				Contents: diff,
 | 
						|
				Method:   models.DetectionMethod(detectionMethod),
 | 
						|
			}
 | 
						|
			o.Packages[name] = pack
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (o *redhat) getDiffChangelog(pack models.Package, availableChangelog string) (string, error) {
 | 
						|
	installedVer := ver.NewVersion(fmt.Sprintf("%s-%s", pack.Version, pack.Release))
 | 
						|
	scanner := bufio.NewScanner(strings.NewReader(availableChangelog))
 | 
						|
	diff := []string{}
 | 
						|
	found := false
 | 
						|
	for scanner.Scan() {
 | 
						|
		line := scanner.Text()
 | 
						|
		if !strings.HasPrefix(line, "* ") {
 | 
						|
			diff = append(diff, line)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		// openssh on RHEL
 | 
						|
		//   openssh-server-6.6.1p1-35.el7_3.x86_64   rhui-rhel-7-server-rhui-rpms
 | 
						|
		//   Wed Mar  1 21:00:00 2017 Jakub Jelen <jjelen@redhat.com> - 6.6.1p1-35 + 0.9.3-9
 | 
						|
		ss := strings.Split(line, " + ")
 | 
						|
		if 1 < len(ss) {
 | 
						|
			line = ss[0]
 | 
						|
		}
 | 
						|
 | 
						|
		ss = strings.Split(line, " ")
 | 
						|
		if len(ss) < 2 {
 | 
						|
			diff = append(diff, line)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		v := ss[len(ss)-1]
 | 
						|
		v = strings.TrimPrefix(v, "-")
 | 
						|
		v = strings.TrimPrefix(v, "[")
 | 
						|
		v = strings.TrimSuffix(v, "]")
 | 
						|
 | 
						|
		// On Amazon often end with email address. <aaa@aaa.com> Go to next line
 | 
						|
		if strings.HasPrefix(v, "<") && strings.HasSuffix(v, ">") {
 | 
						|
			diff = append(diff, line)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		version := ver.NewVersion(v)
 | 
						|
		if installedVer.Equal(version) || installedVer.GreaterThan(version) {
 | 
						|
			found = true
 | 
						|
			break
 | 
						|
		}
 | 
						|
		diff = append(diff, line)
 | 
						|
	}
 | 
						|
 | 
						|
	if len(diff) == 0 || !found {
 | 
						|
		return availableChangelog,
 | 
						|
			fmt.Errorf("Failed to find the version in changelog: %s-%s-%s",
 | 
						|
				pack.Name, pack.Version, pack.Release)
 | 
						|
	}
 | 
						|
	return strings.TrimSpace(strings.Join(diff, "\n")), nil
 | 
						|
}
 | 
						|
 | 
						|
func (o *redhat) scanCveIDsInChangelog(updatable models.Packages) (models.VulnInfos, error) {
 | 
						|
	packCveIDs := make(map[string][]string)
 | 
						|
	for name := range updatable {
 | 
						|
		cveIDs := []string{}
 | 
						|
		pack := o.Packages[name]
 | 
						|
		if pack.Changelog.Method == models.FailedToFindVersionInChangelog {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		scanner := bufio.NewScanner(strings.NewReader(pack.Changelog.Contents))
 | 
						|
		for scanner.Scan() {
 | 
						|
			if matches := cveRe.FindAllString(scanner.Text(), -1); 0 < len(matches) {
 | 
						|
				for _, m := range matches {
 | 
						|
					cveIDs = util.AppendIfMissing(cveIDs, m)
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
		packCveIDs[name] = cveIDs
 | 
						|
	}
 | 
						|
 | 
						|
	// transform datastructure
 | 
						|
	// - From
 | 
						|
	//	  "packname": []{"CVE-2017-1111", ".../
 | 
						|
	//
 | 
						|
	// - To
 | 
						|
	//	   map {
 | 
						|
	//		 "CVE-2017-1111": "packname",
 | 
						|
	//	   }
 | 
						|
	vinfos := models.VulnInfos{}
 | 
						|
	for name, cveIDs := range packCveIDs {
 | 
						|
		for _, cid := range cveIDs {
 | 
						|
			if v, ok := vinfos[cid]; ok {
 | 
						|
				v.PackageNames = append(v.PackageNames, name)
 | 
						|
				vinfos[cid] = v
 | 
						|
			} else {
 | 
						|
				vinfos[cid] = models.VulnInfo{
 | 
						|
					CveID:        cid,
 | 
						|
					PackageNames: []string{name},
 | 
						|
					Confidence:   models.ChangelogExactMatch,
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return vinfos, nil
 | 
						|
}
 | 
						|
 | 
						|
type distroAdvisoryCveIDs struct {
 | 
						|
	DistroAdvisory models.DistroAdvisory
 | 
						|
	CveIDs         []string
 | 
						|
}
 | 
						|
 | 
						|
// Scaning unsecure packages using yum-plugin-security.
 | 
						|
// Amazon, RHEL, Oracle Linux
 | 
						|
func (o *redhat) scanCveIDsByCommands(updatable models.Packages) (models.VulnInfos, error) {
 | 
						|
	if o.Distro.Family == config.CentOS {
 | 
						|
		// CentOS has no security channel.
 | 
						|
		return nil, fmt.Errorf(
 | 
						|
			"yum updateinfo is not suppported on CentOS")
 | 
						|
	}
 | 
						|
 | 
						|
	cmd := "yum --color=never repolist"
 | 
						|
	r := o.exec(util.PrependProxyEnv(cmd), o.sudo())
 | 
						|
	if !r.isSuccess() {
 | 
						|
		return nil, fmt.Errorf("Failed to SSH: %s", r)
 | 
						|
	}
 | 
						|
 | 
						|
	// get advisoryID(RHSA, ALAS, ELSA) - package name,version
 | 
						|
	major, err := (o.Distro.MajorVersion())
 | 
						|
	if err != nil {
 | 
						|
		return nil, fmt.Errorf("Not implemented yet: %s, err: %s", o.Distro, err)
 | 
						|
	}
 | 
						|
 | 
						|
	if (o.Distro.Family == config.RedHat || o.Distro.Family == config.Oracle) && major == 5 {
 | 
						|
		cmd = "yum --color=never list-security --security"
 | 
						|
	} else {
 | 
						|
		cmd = "yum --color=never --security updateinfo list updates"
 | 
						|
	}
 | 
						|
	r = o.exec(util.PrependProxyEnv(cmd), o.sudo())
 | 
						|
	if !r.isSuccess() {
 | 
						|
		return nil, fmt.Errorf("Failed to SSH: %s", r)
 | 
						|
	}
 | 
						|
	advIDPackNamesList, err := o.parseYumUpdateinfoListAvailable(r.Stdout)
 | 
						|
 | 
						|
	dict := make(map[string]models.Packages)
 | 
						|
	for _, advIDPackNames := range advIDPackNamesList {
 | 
						|
		packages := models.Packages{}
 | 
						|
		for _, packName := range advIDPackNames.PackNames {
 | 
						|
			pack, found := updatable[packName]
 | 
						|
			if !found {
 | 
						|
				return nil, fmt.Errorf(
 | 
						|
					"Package not found. pack: %#v", packName)
 | 
						|
			}
 | 
						|
			packages[pack.Name] = pack
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		dict[advIDPackNames.AdvisoryID] = packages
 | 
						|
	}
 | 
						|
 | 
						|
	// get advisoryID(RHSA, ALAS, ELSA) - CVE IDs
 | 
						|
	if (o.Distro.Family == config.RedHat || o.Distro.Family == config.Oracle) && major == 5 {
 | 
						|
		cmd = "yum --color=never info-security"
 | 
						|
	} else {
 | 
						|
		cmd = "yum --color=never --security updateinfo updates"
 | 
						|
	}
 | 
						|
	r = o.exec(util.PrependProxyEnv(cmd), o.sudo())
 | 
						|
	if !r.isSuccess() {
 | 
						|
		return nil, fmt.Errorf("Failed to SSH: %s", r)
 | 
						|
	}
 | 
						|
	advisoryCveIDsList, err := o.parseYumUpdateinfo(r.Stdout)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	// All information collected.
 | 
						|
	// Convert to VulnInfos.
 | 
						|
	vinfos := models.VulnInfos{}
 | 
						|
	for _, advIDCveIDs := range advisoryCveIDsList {
 | 
						|
		for _, cveID := range advIDCveIDs.CveIDs {
 | 
						|
			vinfo, found := vinfos[cveID]
 | 
						|
			if found {
 | 
						|
				advAppended := append(vinfo.DistroAdvisories, advIDCveIDs.DistroAdvisory)
 | 
						|
				vinfo.DistroAdvisories = advAppended
 | 
						|
 | 
						|
				packs := dict[advIDCveIDs.DistroAdvisory.AdvisoryID]
 | 
						|
				for _, pack := range packs {
 | 
						|
					vinfo.PackageNames = append(vinfo.PackageNames, pack.Name)
 | 
						|
				}
 | 
						|
			} else {
 | 
						|
				names := []string{}
 | 
						|
				packs := dict[advIDCveIDs.DistroAdvisory.AdvisoryID]
 | 
						|
				for _, pack := range packs {
 | 
						|
					names = append(names, pack.Name)
 | 
						|
				}
 | 
						|
				vinfo = models.VulnInfo{
 | 
						|
					CveID:            cveID,
 | 
						|
					DistroAdvisories: []models.DistroAdvisory{advIDCveIDs.DistroAdvisory},
 | 
						|
					PackageNames:     names,
 | 
						|
					Confidence:       models.YumUpdateSecurityMatch,
 | 
						|
				}
 | 
						|
			}
 | 
						|
			vinfos[cveID] = vinfo
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return vinfos, nil
 | 
						|
}
 | 
						|
 | 
						|
var horizontalRulePattern = regexp.MustCompile(`^=+$`)
 | 
						|
 | 
						|
func (o *redhat) parseYumUpdateinfo(stdout string) (result []distroAdvisoryCveIDs, err error) {
 | 
						|
	sectionState := Outside
 | 
						|
	lines := strings.Split(stdout, "\n")
 | 
						|
	lines = append(lines, "=============")
 | 
						|
 | 
						|
	// Amazon Linux AMI Security Information
 | 
						|
	advisory := models.DistroAdvisory{}
 | 
						|
 | 
						|
	cveIDsSetInThisSection := make(map[string]bool)
 | 
						|
 | 
						|
	// use this flag to Collect CVE IDs in CVEs field.
 | 
						|
	inDesctiption, inCves := false, false
 | 
						|
 | 
						|
	for _, line := range lines {
 | 
						|
		line = strings.TrimSpace(line)
 | 
						|
 | 
						|
		// find the new section pattern
 | 
						|
		if horizontalRulePattern.MatchString(line) {
 | 
						|
			// set previous section's result to return-variable
 | 
						|
			if sectionState == Content {
 | 
						|
				foundCveIDs := []string{}
 | 
						|
				for cveID := range cveIDsSetInThisSection {
 | 
						|
					foundCveIDs = append(foundCveIDs, cveID)
 | 
						|
				}
 | 
						|
				result = append(result, distroAdvisoryCveIDs{
 | 
						|
					DistroAdvisory: advisory,
 | 
						|
					CveIDs:         foundCveIDs,
 | 
						|
				})
 | 
						|
 | 
						|
				// reset for next section.
 | 
						|
				cveIDsSetInThisSection = make(map[string]bool)
 | 
						|
				inDesctiption, inCves = false, false
 | 
						|
				advisory = models.DistroAdvisory{}
 | 
						|
			}
 | 
						|
 | 
						|
			// Go to next section
 | 
						|
			sectionState = o.changeSectionState(sectionState)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		switch sectionState {
 | 
						|
		case Header:
 | 
						|
			switch o.Distro.Family {
 | 
						|
			case config.CentOS:
 | 
						|
				// CentOS has no security channel.
 | 
						|
				return result, fmt.Errorf(
 | 
						|
					"yum updateinfo is not suppported on  CentOS")
 | 
						|
			case config.RedHat, config.Amazon, config.Oracle:
 | 
						|
				// nop
 | 
						|
			}
 | 
						|
 | 
						|
		case Content:
 | 
						|
			if found := o.isDescriptionLine(line); found {
 | 
						|
				inDesctiption, inCves = true, false
 | 
						|
				ss := strings.Split(line, " : ")
 | 
						|
				advisory.Description += fmt.Sprintf("%s\n",
 | 
						|
					strings.Join(ss[1:len(ss)], " : "))
 | 
						|
				continue
 | 
						|
			}
 | 
						|
 | 
						|
			// severity
 | 
						|
			if severity, found := o.parseYumUpdateinfoToGetSeverity(line); found {
 | 
						|
				advisory.Severity = severity
 | 
						|
				continue
 | 
						|
			}
 | 
						|
 | 
						|
			// No need to parse in description except severity
 | 
						|
			if inDesctiption {
 | 
						|
				if ss := strings.Split(line, ": "); 1 < len(ss) {
 | 
						|
					advisory.Description += fmt.Sprintf("%s\n",
 | 
						|
						strings.Join(ss[1:len(ss)], ": "))
 | 
						|
				}
 | 
						|
				continue
 | 
						|
			}
 | 
						|
 | 
						|
			if found := o.isCvesHeaderLine(line); found {
 | 
						|
				inCves = true
 | 
						|
				ss := strings.Split(line, "CVEs : ")
 | 
						|
				line = strings.Join(ss[1:len(ss)], " ")
 | 
						|
				cveIDs := o.parseYumUpdateinfoLineToGetCveIDs(line)
 | 
						|
				for _, cveID := range cveIDs {
 | 
						|
					cveIDsSetInThisSection[cveID] = true
 | 
						|
				}
 | 
						|
				continue
 | 
						|
			}
 | 
						|
 | 
						|
			if inCves {
 | 
						|
				cveIDs := o.parseYumUpdateinfoLineToGetCveIDs(line)
 | 
						|
				for _, cveID := range cveIDs {
 | 
						|
					cveIDsSetInThisSection[cveID] = true
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			advisoryID, found := o.parseYumUpdateinfoToGetAdvisoryID(line)
 | 
						|
			if found {
 | 
						|
				advisory.AdvisoryID = advisoryID
 | 
						|
				continue
 | 
						|
			}
 | 
						|
 | 
						|
			issued, found := o.parseYumUpdateinfoLineToGetIssued(line)
 | 
						|
			if found {
 | 
						|
				advisory.Issued = issued
 | 
						|
				continue
 | 
						|
			}
 | 
						|
 | 
						|
			updated, found := o.parseYumUpdateinfoLineToGetUpdated(line)
 | 
						|
			if found {
 | 
						|
				advisory.Updated = updated
 | 
						|
				continue
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return
 | 
						|
}
 | 
						|
 | 
						|
// state
 | 
						|
const (
 | 
						|
	Outside = iota
 | 
						|
	Header  = iota
 | 
						|
	Content = iota
 | 
						|
)
 | 
						|
 | 
						|
func (o *redhat) changeSectionState(state int) (newState int) {
 | 
						|
	switch state {
 | 
						|
	case Outside, Content:
 | 
						|
		newState = Header
 | 
						|
	case Header:
 | 
						|
		newState = Content
 | 
						|
	}
 | 
						|
	return newState
 | 
						|
}
 | 
						|
 | 
						|
func (o *redhat) isCvesHeaderLine(line string) bool {
 | 
						|
	return strings.Contains(line, "CVEs : ")
 | 
						|
}
 | 
						|
 | 
						|
var yumCveIDPattern = regexp.MustCompile(`(CVE-\d{4}-\d{4,})`)
 | 
						|
 | 
						|
func (o *redhat) parseYumUpdateinfoLineToGetCveIDs(line string) []string {
 | 
						|
	return yumCveIDPattern.FindAllString(line, -1)
 | 
						|
}
 | 
						|
 | 
						|
var yumAdvisoryIDPattern = regexp.MustCompile(`^ *Update ID : (.*)$`)
 | 
						|
 | 
						|
func (o *redhat) parseYumUpdateinfoToGetAdvisoryID(line string) (advisoryID string, found bool) {
 | 
						|
	result := yumAdvisoryIDPattern.FindStringSubmatch(line)
 | 
						|
	if len(result) != 2 {
 | 
						|
		return "", false
 | 
						|
	}
 | 
						|
	return strings.TrimSpace(result[1]), true
 | 
						|
}
 | 
						|
 | 
						|
var yumIssuedPattern = regexp.MustCompile(`^\s*Issued : (\d{4}-\d{2}-\d{2})`)
 | 
						|
 | 
						|
func (o *redhat) parseYumUpdateinfoLineToGetIssued(line string) (date time.Time, found bool) {
 | 
						|
	return o.parseYumUpdateinfoLineToGetDate(line, yumIssuedPattern)
 | 
						|
}
 | 
						|
 | 
						|
var yumUpdatedPattern = regexp.MustCompile(`^\s*Updated : (\d{4}-\d{2}-\d{2})`)
 | 
						|
 | 
						|
func (o *redhat) parseYumUpdateinfoLineToGetUpdated(line string) (date time.Time, found bool) {
 | 
						|
	return o.parseYumUpdateinfoLineToGetDate(line, yumUpdatedPattern)
 | 
						|
}
 | 
						|
 | 
						|
func (o *redhat) parseYumUpdateinfoLineToGetDate(line string, regexpPattern *regexp.Regexp) (date time.Time, found bool) {
 | 
						|
	result := regexpPattern.FindStringSubmatch(line)
 | 
						|
	if len(result) != 2 {
 | 
						|
		return date, false
 | 
						|
	}
 | 
						|
	t, err := time.Parse("2006-01-02", result[1])
 | 
						|
	if err != nil {
 | 
						|
		return date, false
 | 
						|
	}
 | 
						|
	return t, true
 | 
						|
}
 | 
						|
 | 
						|
var yumDescriptionPattern = regexp.MustCompile(`^\s*Description : `)
 | 
						|
 | 
						|
func (o *redhat) isDescriptionLine(line string) bool {
 | 
						|
	return yumDescriptionPattern.MatchString(line)
 | 
						|
}
 | 
						|
 | 
						|
var yumSeverityPattern = regexp.MustCompile(`^ *Severity : (.*)$`)
 | 
						|
 | 
						|
func (o *redhat) parseYumUpdateinfoToGetSeverity(line string) (severity string, found bool) {
 | 
						|
	result := yumSeverityPattern.FindStringSubmatch(line)
 | 
						|
	if len(result) != 2 {
 | 
						|
		return "", false
 | 
						|
	}
 | 
						|
	return strings.TrimSpace(result[1]), true
 | 
						|
}
 | 
						|
 | 
						|
type advisoryIDPacks struct {
 | 
						|
	AdvisoryID string
 | 
						|
	PackNames  []string
 | 
						|
}
 | 
						|
 | 
						|
type advisoryIDPacksList []advisoryIDPacks
 | 
						|
 | 
						|
func (list advisoryIDPacksList) find(advisoryID string) (advisoryIDPacks, bool) {
 | 
						|
	for _, a := range list {
 | 
						|
		if a.AdvisoryID == advisoryID {
 | 
						|
			return a, true
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return advisoryIDPacks{}, false
 | 
						|
}
 | 
						|
func (o *redhat) extractPackNameVerRel(nameVerRel string) (name, ver, rel string) {
 | 
						|
	fields := strings.Split(nameVerRel, ".")
 | 
						|
	archTrimed := strings.Join(fields[0:len(fields)-1], ".")
 | 
						|
 | 
						|
	fields = strings.Split(archTrimed, "-")
 | 
						|
	rel = fields[len(fields)-1]
 | 
						|
	ver = fields[len(fields)-2]
 | 
						|
	name = strings.Join(fields[0:(len(fields)-2)], "-")
 | 
						|
	return
 | 
						|
}
 | 
						|
 | 
						|
// parseYumUpdateinfoListAvailable collect AdvisorID(RHSA, ALAS, ELSA), packages
 | 
						|
func (o *redhat) parseYumUpdateinfoListAvailable(stdout string) (advisoryIDPacksList, error) {
 | 
						|
	result := []advisoryIDPacks{}
 | 
						|
	lines := strings.Split(stdout, "\n")
 | 
						|
	for _, line := range lines {
 | 
						|
 | 
						|
		if !(strings.HasPrefix(line, "RHSA") ||
 | 
						|
			strings.HasPrefix(line, "ALAS") ||
 | 
						|
			strings.HasPrefix(line, "ELSA")) {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		fields := strings.Fields(line)
 | 
						|
		if len(fields) != 3 {
 | 
						|
			return []advisoryIDPacks{}, fmt.Errorf(
 | 
						|
				"Unknown format. line: %s", line)
 | 
						|
		}
 | 
						|
 | 
						|
		// extract fields
 | 
						|
		advisoryID := fields[0]
 | 
						|
		packVersion := fields[2]
 | 
						|
		packName, _, _ := o.extractPackNameVerRel(packVersion)
 | 
						|
 | 
						|
		found := false
 | 
						|
		for i, s := range result {
 | 
						|
			if s.AdvisoryID == advisoryID {
 | 
						|
				names := s.PackNames
 | 
						|
				names = append(names, packName)
 | 
						|
				result[i].PackNames = names
 | 
						|
				found = true
 | 
						|
				break
 | 
						|
			}
 | 
						|
		}
 | 
						|
		if !found {
 | 
						|
			result = append(result, advisoryIDPacks{
 | 
						|
				AdvisoryID: advisoryID,
 | 
						|
				PackNames:  []string{packName},
 | 
						|
			})
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return result, nil
 | 
						|
}
 | 
						|
 | 
						|
func (o *redhat) clone() osTypeInterface {
 | 
						|
	return o
 | 
						|
}
 | 
						|
 | 
						|
func (o *redhat) sudo() bool {
 | 
						|
	switch o.Distro.Family {
 | 
						|
	case config.Amazon, config.CentOS:
 | 
						|
		return false
 | 
						|
	default:
 | 
						|
		// RHEL
 | 
						|
		return true
 | 
						|
	}
 | 
						|
}
 |