1023 lines
27 KiB
Go
1023 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 (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/future-architect/vuls/config"
|
|
"github.com/future-architect/vuls/models"
|
|
"github.com/future-architect/vuls/util"
|
|
|
|
"github.com/k0kubun/pp"
|
|
)
|
|
|
|
// inherit OsTypeInterface
|
|
type redhat struct {
|
|
base
|
|
}
|
|
|
|
// NewRedhat is constructor
|
|
func newRedhat(c config.ServerInfo) *redhat {
|
|
r := &redhat{}
|
|
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("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("oraclelinux", 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("centos", release)
|
|
default:
|
|
red.setDistro("rhel", release)
|
|
}
|
|
return true, red
|
|
}
|
|
return true, red
|
|
}
|
|
|
|
if r := exec(c, "ls /etc/system-release", noSudo); r.isSuccess() {
|
|
family := "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 !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 "centos":
|
|
cmds = []cmd{
|
|
{"yum --changelog --assumeno update yum", []int{0, 1}},
|
|
}
|
|
|
|
case "rhel", "oraclelinux":
|
|
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 check-update", []int{0, 100}},
|
|
{"yum --color=never list-security --security", zero},
|
|
{"yum --color=never info-security", zero},
|
|
}
|
|
} else {
|
|
cmds = []cmd{
|
|
{"yum --color=never repolist", zero},
|
|
{"yum --color=never check-update", []int{0, 100}},
|
|
{"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
|
|
}
|
|
|
|
// CentOS 6, 7 ... yum-plugin-changelog
|
|
// RHEL 5 ... yum-security
|
|
// RHEL 6, 7 ... -
|
|
// Amazon ... -
|
|
func (o *redhat) checkDependencies() error {
|
|
var packName string
|
|
if o.Distro.Family == "amazon" {
|
|
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 == "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)
|
|
}
|
|
}
|
|
|
|
switch o.Distro.Family {
|
|
case "centos":
|
|
packName = "yum-plugin-changelog"
|
|
case "rhel", "oraclelinux":
|
|
if majorVersion < 6 {
|
|
packName = "yum-security"
|
|
} else {
|
|
// yum-plugin-security is installed by default on RHEL6, 7
|
|
return nil
|
|
}
|
|
default:
|
|
return fmt.Errorf("Not implemented yet: %s", o.Distro)
|
|
}
|
|
|
|
cmd := "rpm -q " + packName
|
|
if r := o.exec(cmd, noSudo); !r.isSuccess() {
|
|
msg := fmt.Sprintf("%s is not installed", packName)
|
|
o.log.Errorf(msg)
|
|
return fmt.Errorf(msg)
|
|
}
|
|
o.log.Infof("Dependencies... Pass")
|
|
return nil
|
|
}
|
|
|
|
func (o *redhat) scanPackages() error {
|
|
var err error
|
|
var packs []models.Package
|
|
if packs, err = o.scanInstalledPackages(); err != nil {
|
|
o.log.Errorf("Failed to scan installed packages")
|
|
return err
|
|
}
|
|
o.setPackages(packs)
|
|
|
|
var vinfos []models.VulnInfo
|
|
if vinfos, err = o.scanVulnInfos(); err != nil {
|
|
o.log.Errorf("Failed to scan vulnerable packages")
|
|
return err
|
|
}
|
|
o.setVulnInfos(vinfos)
|
|
return nil
|
|
}
|
|
|
|
func (o *redhat) scanInstalledPackages() (installedPackages models.Packages, err error) {
|
|
cmd := "rpm -qa --queryformat '%{NAME}\t%{EPOCHNUM}\t%{VERSION}\t%{RELEASE}\n'"
|
|
r := o.exec(cmd, noSudo)
|
|
if r.isSuccess() {
|
|
// e.g.
|
|
// openssl 1.0.1e 30.el6.11
|
|
lines := strings.Split(r.Stdout, "\n")
|
|
for _, line := range lines {
|
|
if trimed := strings.TrimSpace(line); len(trimed) != 0 {
|
|
var pack models.Package
|
|
if pack, err = o.parseScannedPackagesLine(line); err != nil {
|
|
return
|
|
}
|
|
installedPackages = append(installedPackages, pack)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
return installedPackages, fmt.Errorf(
|
|
"Scan packages failed. status: %d, stdout: %s, stderr: %s",
|
|
r.ExitStatus, r.Stdout, r.Stderr)
|
|
}
|
|
|
|
func (o *redhat) parseScannedPackagesLine(line string) (models.Package, error) {
|
|
fields := strings.Fields(line)
|
|
if len(fields) != 4 {
|
|
return models.Package{},
|
|
fmt.Errorf("Failed to parse package line: %s", line)
|
|
}
|
|
ver := ""
|
|
if fields[1] == "0" {
|
|
ver = fields[2]
|
|
} else {
|
|
ver = fmt.Sprintf("%s:%s", fields[1], fields[2])
|
|
}
|
|
return models.Package{
|
|
Name: fields[0],
|
|
Version: ver,
|
|
Release: fields[3],
|
|
}, nil
|
|
}
|
|
|
|
func (o *redhat) scanVulnInfos() ([]models.VulnInfo, error) {
|
|
if o.Distro.Family != "centos" {
|
|
// Amazon, RHEL, Oracle Linux has yum updateinfo as default
|
|
// yum updateinfo can collenct vendor advisory information.
|
|
return o.scanUnsecurePackagesUsingYumPluginSecurity()
|
|
}
|
|
// CentOS does not have security channel...
|
|
// So, yum check-update then parse chnagelog.
|
|
return o.scanUnsecurePackagesUsingYumCheckUpdate()
|
|
}
|
|
|
|
// For CentOS
|
|
func (o *redhat) scanUnsecurePackagesUsingYumCheckUpdate() (models.VulnInfos, error) {
|
|
cmd := "LANGUAGE=en_US.UTF-8 yum --color=never %s check-update"
|
|
if o.getServerInfo().Enablerepo != "" {
|
|
cmd = fmt.Sprintf(cmd, "--enablerepo="+o.getServerInfo().Enablerepo)
|
|
} else {
|
|
cmd = fmt.Sprintf(cmd, "")
|
|
}
|
|
|
|
r := o.exec(util.PrependProxyEnv(cmd), noSudo)
|
|
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)
|
|
}
|
|
|
|
// get Updateble package name, installed, candidate version.
|
|
packages, err := o.parseYumCheckUpdateLines(r.Stdout)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to parse %s. err: %s", cmd, err)
|
|
}
|
|
o.log.Debugf("%s", pp.Sprintf("%v", packages))
|
|
|
|
// set candidate version info
|
|
o.Packages.MergeNewVersion(packages)
|
|
|
|
// Collect CVE-IDs in changelog
|
|
type PackageCveIDs struct {
|
|
Package models.Package
|
|
CveIDs []string
|
|
}
|
|
|
|
allChangelog, err := o.getAllChangelog(packages)
|
|
if err != nil {
|
|
o.log.Errorf("Failed to getAllchangelog. err: %s", err)
|
|
return nil, err
|
|
}
|
|
|
|
// { packageName: changelog-lines }
|
|
var rpm2changelog map[string]*string
|
|
rpm2changelog, err = o.divideChangelogByPackage(allChangelog)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to parseAllChangelog. err: %s", err)
|
|
}
|
|
|
|
for name, clog := range rpm2changelog {
|
|
for i, p := range o.Packages {
|
|
n := fmt.Sprintf("%s-%s-%s",
|
|
p.Name, p.NewVersion, p.NewRelease)
|
|
if name == n {
|
|
o.Packages[i].Changelog = models.Changelog{
|
|
Contents: *clog,
|
|
Method: models.ChangelogExactMatchStr,
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
var results []PackageCveIDs
|
|
for i, pack := range packages {
|
|
changelog := o.getChangelogCVELines(rpm2changelog, pack)
|
|
|
|
// Collect unique set of CVE-ID in each changelog
|
|
uniqueCveIDMap := make(map[string]bool)
|
|
lines := strings.Split(changelog, "\n")
|
|
for _, line := range lines {
|
|
cveIDs := o.parseYumUpdateinfoLineToGetCveIDs(line)
|
|
for _, c := range cveIDs {
|
|
uniqueCveIDMap[c] = true
|
|
}
|
|
}
|
|
|
|
// keys
|
|
var cveIDs []string
|
|
for k := range uniqueCveIDMap {
|
|
cveIDs = append(cveIDs, k)
|
|
}
|
|
p := PackageCveIDs{
|
|
Package: pack,
|
|
CveIDs: cveIDs,
|
|
}
|
|
results = append(results, p)
|
|
|
|
o.log.Infof("(%d/%d) Scanned %s-%s-%s -> %s-%s : %s",
|
|
i+1,
|
|
len(packages),
|
|
p.Package.Name,
|
|
p.Package.Version,
|
|
p.Package.Release,
|
|
p.Package.NewVersion,
|
|
p.Package.NewRelease,
|
|
p.CveIDs)
|
|
}
|
|
|
|
// transform datastructure
|
|
// - From
|
|
// [
|
|
// {
|
|
// Pack: models.Packages,
|
|
// CveIDs: []string,
|
|
// },
|
|
// ]
|
|
// - To
|
|
// map {
|
|
// CveID: []models.Package
|
|
// }
|
|
cveIDPackMap := make(map[string][]models.Package)
|
|
for _, res := range results {
|
|
for _, cveID := range res.CveIDs {
|
|
cveIDPackMap[cveID] = append(
|
|
cveIDPackMap[cveID], res.Package)
|
|
}
|
|
}
|
|
|
|
vinfos := []models.VulnInfo{}
|
|
for k, v := range cveIDPackMap {
|
|
// Amazon, RHEL do not use this method, so VendorAdvisory do not set.
|
|
vinfos = append(vinfos, models.VulnInfo{
|
|
CveID: k,
|
|
Packages: v,
|
|
Confidence: models.ChangelogExactMatch,
|
|
})
|
|
}
|
|
return vinfos, nil
|
|
}
|
|
|
|
// parseYumCheckUpdateLines parse yum check-update to get package name, candidate version
|
|
func (o *redhat) parseYumCheckUpdateLines(stdout string) (results models.Packages, err error) {
|
|
needToParse := false
|
|
lines := strings.Split(stdout, "\n")
|
|
for _, line := range lines {
|
|
// update information of packages begin after blank line.
|
|
if trimed := strings.TrimSpace(line); len(trimed) == 0 {
|
|
needToParse = true
|
|
continue
|
|
}
|
|
if needToParse {
|
|
if strings.HasPrefix(line, "Obsoleting") ||
|
|
strings.HasPrefix(line, "Security:") {
|
|
// see https://github.com/future-architect/vuls/issues/165
|
|
continue
|
|
}
|
|
candidate, err := o.parseYumCheckUpdateLine(line)
|
|
if err != nil {
|
|
return results, err
|
|
}
|
|
|
|
installed, found := o.Packages.FindByName(candidate.Name)
|
|
if !found {
|
|
o.log.Warnf("Not found the package in rpm -qa. candidate: %s-%s-%s",
|
|
candidate.Name, candidate.Version, candidate.Release)
|
|
results = append(results, candidate)
|
|
continue
|
|
}
|
|
installed.NewVersion = candidate.NewVersion
|
|
installed.NewRelease = candidate.NewRelease
|
|
installed.Repository = candidate.Repository
|
|
results = append(results, installed)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (o *redhat) parseYumCheckUpdateLine(line string) (models.Package, error) {
|
|
fields := strings.Fields(line)
|
|
if len(fields) < 3 {
|
|
return models.Package{}, fmt.Errorf("Unknown format: %s", line)
|
|
}
|
|
splitted := strings.Split(fields[0], ".")
|
|
packName := ""
|
|
if len(splitted) == 1 {
|
|
packName = fields[0]
|
|
} else {
|
|
packName = strings.Join(strings.Split(fields[0], ".")[0:(len(splitted)-1)], ".")
|
|
}
|
|
|
|
verfields := strings.Split(fields[1], "-")
|
|
if len(verfields) != 2 {
|
|
return models.Package{}, fmt.Errorf("Unknown format: %s", line)
|
|
}
|
|
release := verfields[1]
|
|
repos := strings.Join(fields[2:len(fields)], " ")
|
|
|
|
return models.Package{
|
|
Name: packName,
|
|
NewVersion: verfields[0],
|
|
NewRelease: release,
|
|
Repository: repos,
|
|
}, nil
|
|
}
|
|
|
|
func (o *redhat) mkPstring() *string {
|
|
str := ""
|
|
return &str
|
|
}
|
|
|
|
func (o *redhat) regexpReplace(src string, pat string, rep string) string {
|
|
re := regexp.MustCompile(pat)
|
|
return re.ReplaceAllString(src, rep)
|
|
}
|
|
|
|
var changeLogCVEPattern = regexp.MustCompile(`CVE-[0-9]+-[0-9]+`)
|
|
|
|
func (o *redhat) getChangelogCVELines(rpm2changelog map[string]*string, pack models.Package) string {
|
|
rpm := fmt.Sprintf("%s-%s-%s", pack.Name, pack.NewVersion, pack.NewRelease)
|
|
retLine := ""
|
|
if rpm2changelog[rpm] != nil {
|
|
lines := strings.Split(*rpm2changelog[rpm], "\n")
|
|
for _, line := range lines {
|
|
if changeLogCVEPattern.MatchString(line) {
|
|
retLine += fmt.Sprintf("%s\n", line)
|
|
}
|
|
}
|
|
}
|
|
return retLine
|
|
}
|
|
|
|
func (o *redhat) divideChangelogByPackage(allChangelog string) (map[string]*string, error) {
|
|
var majorVersion int
|
|
var err error
|
|
if o.Distro.Family == "centos" {
|
|
majorVersion, err = o.Distro.MajorVersion()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Not implemented yet: %s, err: %s", o.Distro, err)
|
|
}
|
|
}
|
|
|
|
orglines := strings.Split(allChangelog, "\n")
|
|
tmpline := ""
|
|
var lines []string
|
|
var prev, now bool
|
|
for i := range orglines {
|
|
if majorVersion == 5 {
|
|
/* for CentOS5 (yum-util < 1.1.20) */
|
|
prev = false
|
|
now = false
|
|
if 0 < i {
|
|
prev, err = o.isRpmPackageNameLine(orglines[i-1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
now, err = o.isRpmPackageNameLine(orglines[i])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if prev && now {
|
|
tmpline = fmt.Sprintf("%s, %s", tmpline, orglines[i])
|
|
continue
|
|
}
|
|
if !prev && now {
|
|
tmpline = fmt.Sprintf("%s%s", tmpline, orglines[i])
|
|
continue
|
|
}
|
|
if tmpline != "" {
|
|
lines = append(lines, fmt.Sprintf("%s", tmpline))
|
|
tmpline = ""
|
|
}
|
|
lines = append(lines, fmt.Sprintf("%s", orglines[i]))
|
|
} else {
|
|
/* for CentOS6,7 (yum-util >= 1.1.20) */
|
|
line := orglines[i]
|
|
line = o.regexpReplace(line, `^ChangeLog for: `, "")
|
|
line = o.regexpReplace(line, `^\*\*\sNo\sChangeLog\sfor:.*`, "")
|
|
lines = append(lines, line)
|
|
}
|
|
}
|
|
|
|
rpm2changelog := make(map[string]*string)
|
|
writePointer := o.mkPstring()
|
|
for _, line := range lines {
|
|
match, err := o.isRpmPackageNameLine(line)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if match {
|
|
rpms := strings.Split(line, ",")
|
|
pNewString := o.mkPstring()
|
|
writePointer = pNewString
|
|
for _, rpm := range rpms {
|
|
rpm = strings.TrimSpace(rpm)
|
|
rpm = o.regexpReplace(rpm, `\.(i386|i486|i586|i686|k6|athlon|x86_64|noarch|ppc|alpha|sparc)$`, "")
|
|
if ss := strings.Split(rpm, ":"); 1 < len(ss) {
|
|
epoch := ss[0]
|
|
packVersion := strings.Join(ss[1:len(ss)], ":")
|
|
if sss := strings.Split(packVersion, "-"); 2 < len(sss) {
|
|
version := strings.Join(sss[len(sss)-2:len(sss)], "-")
|
|
name := strings.Join(sss[0:len(sss)-2], "-")
|
|
rpm = fmt.Sprintf("%s-%s:%s", name, epoch, version)
|
|
}
|
|
}
|
|
|
|
rpm2changelog[rpm] = pNewString
|
|
}
|
|
} else {
|
|
if strings.HasPrefix(line, "Dependencies Resolved") {
|
|
return rpm2changelog, nil
|
|
}
|
|
*writePointer += fmt.Sprintf("%s\n", line)
|
|
}
|
|
}
|
|
return rpm2changelog, nil
|
|
}
|
|
|
|
// CentOS
|
|
func (o *redhat) getAllChangelog(packages models.Packages) (stdout string, err error) {
|
|
packageNames := ""
|
|
for _, pack := range packages {
|
|
packageNames += fmt.Sprintf("%s ", pack.Name)
|
|
}
|
|
|
|
command := ""
|
|
if 0 < len(config.Conf.HTTPProxy) {
|
|
command += util.ProxyEnv()
|
|
}
|
|
|
|
yumopts := ""
|
|
if o.getServerInfo().Enablerepo != "" {
|
|
yumopts = " --enablerepo=" + o.getServerInfo().Enablerepo
|
|
}
|
|
if config.Conf.SkipBroken {
|
|
yumopts += " --skip-broken"
|
|
}
|
|
|
|
// yum update --changelog doesn't have --color option.
|
|
command += fmt.Sprintf(" LANGUAGE=en_US.UTF-8 yum --changelog --assumeno update %s ", yumopts) + packageNames
|
|
|
|
r := o.exec(command, sudo)
|
|
if !r.isSuccess(0, 1) {
|
|
return "", fmt.Errorf(
|
|
"Failed to get changelog. status: %d, stdout: %s, stderr: %s",
|
|
r.ExitStatus, r.Stdout, r.Stderr)
|
|
}
|
|
return strings.Replace(r.Stdout, "\r", "", -1), nil
|
|
}
|
|
|
|
type distroAdvisoryCveIDs struct {
|
|
DistroAdvisory models.DistroAdvisory
|
|
CveIDs []string
|
|
}
|
|
|
|
// Scaning unsecure packages using yum-plugin-security.
|
|
// Amazon, RHEL, Oracle Linux
|
|
func (o *redhat) scanUnsecurePackagesUsingYumPluginSecurity() (models.VulnInfos, error) {
|
|
if o.Distro.Family == "centos" {
|
|
// CentOS has no security channel.
|
|
// So use yum check-update && parse changelog
|
|
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 == "rhel" || o.Distro.Family == "oraclelinux") && 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)
|
|
|
|
// get package name, version, rel to be upgrade.
|
|
cmd = "LANGUAGE=en_US.UTF-8 yum --color=never check-update"
|
|
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)
|
|
}
|
|
updatable, err := o.parseYumCheckUpdateLines(r.Stdout)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Failed to parse %s. err: %s", cmd, err)
|
|
}
|
|
o.log.Debugf("%s", pp.Sprintf("%v", updatable))
|
|
|
|
// set candidate version info
|
|
o.Packages.MergeNewVersion(updatable)
|
|
|
|
dict := map[string][]models.Package{}
|
|
for _, advIDPackNames := range advIDPackNamesList {
|
|
packages := models.Packages{}
|
|
for _, packName := range advIDPackNames.PackNames {
|
|
pack, found := updatable.FindByName(packName)
|
|
if !found {
|
|
return nil, fmt.Errorf(
|
|
"Package not found. pack: %#v", packName)
|
|
}
|
|
packages = append(packages, pack)
|
|
continue
|
|
}
|
|
dict[advIDPackNames.AdvisoryID] = packages
|
|
}
|
|
|
|
// get advisoryID(RHSA, ALAS, ELSA) - CVE IDs
|
|
if (o.Distro.Family == "rhel" || o.Distro.Family == "oraclelinux") && 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
|
|
}
|
|
// pp.Println(advisoryCveIDsList)
|
|
|
|
// All information collected.
|
|
// Convert to VulnInfos.
|
|
vinfos := models.VulnInfos{}
|
|
for _, advIDCveIDs := range advisoryCveIDsList {
|
|
for _, cveID := range advIDCveIDs.CveIDs {
|
|
found := false
|
|
for i, p := range vinfos {
|
|
if cveID == p.CveID {
|
|
advAppended := append(p.DistroAdvisories, advIDCveIDs.DistroAdvisory)
|
|
vinfos[i].DistroAdvisories = advAppended
|
|
|
|
packs := dict[advIDCveIDs.DistroAdvisory.AdvisoryID]
|
|
vinfos[i].Packages = append(vinfos[i].Packages, packs...)
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
cpinfo := models.VulnInfo{
|
|
CveID: cveID,
|
|
DistroAdvisories: []models.DistroAdvisory{advIDCveIDs.DistroAdvisory},
|
|
Packages: dict[advIDCveIDs.DistroAdvisory.AdvisoryID],
|
|
Confidence: models.YumUpdateSecurityMatch,
|
|
}
|
|
vinfos = append(vinfos, cpinfo)
|
|
}
|
|
|
|
}
|
|
}
|
|
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.
|
|
var inDesctiption = 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)
|
|
}
|
|
//TODO remove
|
|
// sort.Strings(foundCveIDs)
|
|
|
|
result = append(result, distroAdvisoryCveIDs{
|
|
DistroAdvisory: advisory,
|
|
CveIDs: foundCveIDs,
|
|
})
|
|
|
|
// reset for next section.
|
|
cveIDsSetInThisSection = make(map[string]bool)
|
|
inDesctiption = false
|
|
}
|
|
|
|
// Go to next section
|
|
sectionState = o.changeSectionState(sectionState)
|
|
continue
|
|
}
|
|
|
|
switch sectionState {
|
|
case Header:
|
|
switch o.Distro.Family {
|
|
case "centos":
|
|
// CentOS has no security channel.
|
|
// So use yum check-update && parse changelog
|
|
return result, fmt.Errorf(
|
|
"yum updateinfo is not suppported on CentOS")
|
|
case "rhel", "amazon", "oraclelinux":
|
|
// nop
|
|
}
|
|
|
|
case Content:
|
|
if found := o.isDescriptionLine(line); found {
|
|
inDesctiption = true
|
|
}
|
|
|
|
// severity
|
|
severity, found := o.parseYumUpdateinfoToGetSeverity(line)
|
|
if found {
|
|
advisory.Severity = severity
|
|
}
|
|
|
|
// No need to parse in description except severity
|
|
if inDesctiption {
|
|
continue
|
|
}
|
|
|
|
cveIDs := o.parseYumUpdateinfoLineToGetCveIDs(line)
|
|
for _, cveID := range cveIDs {
|
|
cveIDsSetInThisSection[cveID] = true
|
|
}
|
|
|
|
advisoryID, found := o.parseYumUpdateinfoToGetAdvisoryID(line)
|
|
if found {
|
|
advisory.AdvisoryID = advisoryID
|
|
}
|
|
|
|
issued, found := o.parseYumUpdateinfoLineToGetIssued(line)
|
|
if found {
|
|
advisory.Issued = issued
|
|
}
|
|
|
|
updated, found := o.parseYumUpdateinfoLineToGetUpdated(line)
|
|
if found {
|
|
advisory.Updated = updated
|
|
}
|
|
}
|
|
}
|
|
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
|
|
}
|
|
|
|
var rpmPackageArchPattern = regexp.MustCompile(
|
|
`^[^ ]+\.(i386|i486|i586|i686|k6|athlon|x86_64|noarch|ppc|alpha|sparc)$`)
|
|
|
|
func (o *redhat) isRpmPackageNameLine(line string) (bool, error) {
|
|
s := strings.TrimPrefix(line, "ChangeLog for: ")
|
|
ss := strings.Split(s, ", ")
|
|
if len(ss) == 0 {
|
|
return false, nil
|
|
}
|
|
for _, s := range ss {
|
|
s = strings.TrimRight(s, " \r\n")
|
|
if !rpmPackageArchPattern.MatchString(s) {
|
|
return false, nil
|
|
}
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
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 "amazon":
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
}
|