Support SUSE Enterprise Linux (#487)
* Support SUSE Enterprise Linux * Implement Reboot Required detection on SLES * Fix query OVAL because SUSE provides OVAL data each major.minor version * Update README * Support SUSE Enterprise 11
This commit is contained in:
@@ -280,14 +280,7 @@ 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)
|
||||
r := o.exec(rpmQa(o.Distro), noSudo)
|
||||
if !r.isSuccess() {
|
||||
return nil, fmt.Errorf("Scan packages failed: %s", r)
|
||||
}
|
||||
@@ -304,14 +297,13 @@ func (o *redhat) scanInstalledPackages() (models.Packages, error) {
|
||||
// Kernel package may be isntalled multiple versions.
|
||||
// From the viewpoint of vulnerability detection,
|
||||
// pay attention only to the running kernel
|
||||
if pack.Name == "kernel" {
|
||||
ver := fmt.Sprintf("%s-%s.%s", pack.Version, pack.Release, pack.Arch)
|
||||
if o.Kernel.Release != ver {
|
||||
o.log.Debugf("Not a running kernel: %s, uname: %s", ver, release)
|
||||
isKernel, running := isRunningKernel(pack, o.Distro.Family, o.Kernel)
|
||||
if isKernel {
|
||||
if !running {
|
||||
o.log.Debugf("Not a running kernel. pack: %#v, kernel: %#v", pack, o.Kernel)
|
||||
continue
|
||||
} else {
|
||||
o.log.Debugf("Running kernel: %s, uname: %s", ver, release)
|
||||
}
|
||||
o.log.Debugf("Found a running kernel. pack: %#v, kernel: %#v", pack, o.Kernel)
|
||||
}
|
||||
installed[pack.Name] = pack
|
||||
}
|
||||
|
||||
@@ -89,6 +89,11 @@ func detectOS(c config.ServerInfo) (osType osTypeInterface) {
|
||||
return
|
||||
}
|
||||
|
||||
if itsMe, osType = detectSUSE(c); itsMe {
|
||||
util.Log.Debugf("SUSE Linux. Host: %s:%s", c.Host, c.Port)
|
||||
return
|
||||
}
|
||||
|
||||
if itsMe, osType = detectFreebsd(c); itsMe {
|
||||
util.Log.Debugf("FreeBSD. Host: %s:%s", c.Host, c.Port)
|
||||
return
|
||||
|
||||
185
scan/suse.go
Normal file
185
scan/suse.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package scan
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/future-architect/vuls/config"
|
||||
"github.com/future-architect/vuls/models"
|
||||
"github.com/future-architect/vuls/util"
|
||||
)
|
||||
|
||||
// inherit OsTypeInterface
|
||||
type suse struct {
|
||||
redhat
|
||||
}
|
||||
|
||||
// NewRedhat is constructor
|
||||
func newSUSE(c config.ServerInfo) *suse {
|
||||
r := &suse{
|
||||
redhat: redhat{
|
||||
base: base{
|
||||
osPackages: osPackages{
|
||||
Packages: models.Packages{},
|
||||
VulnInfos: models.VulnInfos{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
r.log = util.NewCustomLogger(c)
|
||||
r.setServerInfo(c)
|
||||
return r
|
||||
}
|
||||
|
||||
// https://github.com/mizzy/specinfra/blob/master/lib/specinfra/helper/detect_os/suse.rb
|
||||
func detectSUSE(c config.ServerInfo) (itsMe bool, suse osTypeInterface) {
|
||||
suse = newSUSE(c)
|
||||
|
||||
if r := exec(c, "ls /etc/os-release", noSudo); r.isSuccess() {
|
||||
if r := exec(c, "zypper -V", noSudo); r.isSuccess() {
|
||||
if r := exec(c, "cat /etc/os-release", noSudo); r.isSuccess() {
|
||||
name := ""
|
||||
if strings.Contains(r.Stdout, "ID=opensuse") {
|
||||
//TODO check opensuse or opensuse.leap
|
||||
name = config.OpenSUSE
|
||||
} else if strings.Contains(r.Stdout, `NAME="SLES"`) {
|
||||
name = config.SUSEEnterpriseServer
|
||||
} else {
|
||||
util.Log.Warn("Failed to parse SUSE edition: %s", r)
|
||||
return true, suse
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(`VERSION_ID=\"(\d+\.\d+|\d+)\"`)
|
||||
result := re.FindStringSubmatch(strings.TrimSpace(r.Stdout))
|
||||
if len(result) != 2 {
|
||||
util.Log.Warn("Failed to parse SUSE Linux version: %s", r)
|
||||
return true, suse
|
||||
}
|
||||
suse.setDistro(name, result[1])
|
||||
return true, suse
|
||||
}
|
||||
}
|
||||
} else if r := exec(c, "ls /etc/SuSE-release", noSudo); r.isSuccess() {
|
||||
if r := exec(c, "zypper -V", noSudo); r.isSuccess() {
|
||||
if r := exec(c, "cat /etc/SuSE-release", noSudo); r.isSuccess() {
|
||||
re := regexp.MustCompile(`openSUSE (\d+\.\d+|\d+)`)
|
||||
result := re.FindStringSubmatch(strings.TrimSpace(r.Stdout))
|
||||
if len(result) == 2 {
|
||||
//TODO check opensuse or opensuse.leap
|
||||
suse.setDistro(config.OpenSUSE, result[1])
|
||||
return true, suse
|
||||
}
|
||||
|
||||
re = regexp.MustCompile(`VERSION = (\d+)`)
|
||||
result = re.FindStringSubmatch(strings.TrimSpace(r.Stdout))
|
||||
if len(result) == 2 {
|
||||
version := result[1]
|
||||
re = regexp.MustCompile(`PATCHLEVEL = (\d+)`)
|
||||
result = re.FindStringSubmatch(strings.TrimSpace(r.Stdout))
|
||||
if len(result) == 2 {
|
||||
suse.setDistro(config.SUSEEnterpriseServer,
|
||||
fmt.Sprintf("%s.%s", version, result[1]))
|
||||
return true, suse
|
||||
}
|
||||
}
|
||||
util.Log.Warn("Failed to parse SUSE Linux version: %s", r)
|
||||
return true, suse
|
||||
}
|
||||
}
|
||||
}
|
||||
util.Log.Debugf("Not SUSE Linux. servername: %s", c.ServerName)
|
||||
return false, suse
|
||||
}
|
||||
|
||||
func (o *suse) checkDependencies() error {
|
||||
o.log.Infof("Dependencies... No need")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *suse) checkIfSudoNoPasswd() error {
|
||||
// SUSE doesn't need root privilege
|
||||
o.log.Infof("sudo ... No need")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *suse) scanPackages() error {
|
||||
installed, err := o.scanInstalledPackages()
|
||||
if err != nil {
|
||||
o.log.Errorf("Failed to scan installed packages: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
rebootRequired, err := o.rebootRequired()
|
||||
if err != nil {
|
||||
o.log.Errorf("Failed to detect the kernel reboot required: %s", err)
|
||||
return err
|
||||
}
|
||||
o.Kernel.RebootRequired = rebootRequired
|
||||
|
||||
updatable, err := o.scanUpdatablePackages()
|
||||
if err != nil {
|
||||
o.log.Errorf("Failed to scan updatable packages: %s", err)
|
||||
return err
|
||||
}
|
||||
installed.MergeNewVersion(updatable)
|
||||
o.Packages = installed
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *suse) rebootRequired() (bool, error) {
|
||||
r := o.exec("rpm -q --last kernel-default | head -n1", noSudo)
|
||||
if !r.isSuccess() {
|
||||
return false, fmt.Errorf("Failed to detect the last installed kernel : %v", r)
|
||||
}
|
||||
stdout := strings.Fields(r.Stdout)[0]
|
||||
return !strings.Contains(stdout, strings.TrimSuffix(o.Kernel.Release, "-default")), nil
|
||||
}
|
||||
|
||||
func (o *suse) scanUpdatablePackages() (models.Packages, error) {
|
||||
cmd := ""
|
||||
if v, _ := o.Distro.MajorVersion(); v < 12 {
|
||||
cmd = "zypper -q lu"
|
||||
} else {
|
||||
cmd = "zypper --no-color -q lu"
|
||||
}
|
||||
r := o.exec(cmd, noSudo)
|
||||
if !r.isSuccess() {
|
||||
return nil, fmt.Errorf("Failed to scan updatable packages: %v", r)
|
||||
}
|
||||
return o.parseZypperLULines(r.Stdout)
|
||||
}
|
||||
|
||||
func (o *suse) parseZypperLULines(stdout string) (models.Packages, error) {
|
||||
updatables := models.Packages{}
|
||||
scanner := bufio.NewScanner(strings.NewReader(stdout))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "S | Repository") ||
|
||||
strings.HasPrefix(line, "--+----------------") {
|
||||
continue
|
||||
}
|
||||
pack, err := o.parseZypperLUOneLine(line)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
updatables[pack.Name] = *pack
|
||||
}
|
||||
return updatables, nil
|
||||
}
|
||||
|
||||
func (o *suse) parseZypperLUOneLine(line string) (*models.Package, error) {
|
||||
fs := strings.Fields(line)
|
||||
if len(fs) != 11 {
|
||||
return nil, fmt.Errorf("zypper -q lu Unknown format: %s", line)
|
||||
}
|
||||
available := strings.Split(fs[8], "-")
|
||||
return &models.Package{
|
||||
Name: fs[4],
|
||||
NewVersion: available[0],
|
||||
NewRelease: available[1],
|
||||
Arch: fs[10],
|
||||
}, nil
|
||||
}
|
||||
106
scan/suse_test.go
Normal file
106
scan/suse_test.go
Normal file
@@ -0,0 +1,106 @@
|
||||
/* 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 (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/future-architect/vuls/config"
|
||||
"github.com/future-architect/vuls/models"
|
||||
"github.com/k0kubun/pp"
|
||||
)
|
||||
|
||||
func TestScanUpdatablePackages(t *testing.T) {
|
||||
r := newSUSE(config.ServerInfo{})
|
||||
r.Distro = config.Distro{Family: "sles"}
|
||||
stdout := `S | Repository | Name | Current Version | Available Version | Arch
|
||||
--+---------------------------------------------+-------------------------------+-----------------------------+-----------------------------+-------
|
||||
v | SLES12-SP2-Updates | SUSEConnect | 0.3.0-19.8.1 | 0.3.1-19.11.2 | x86_64
|
||||
v | SLES12-SP2-Updates | SuSEfirewall2 | 3.6.312-2.3.1 | 3.6.312-2.10.1 | noarch`
|
||||
|
||||
var tests = []struct {
|
||||
in string
|
||||
out models.Packages
|
||||
}{
|
||||
{
|
||||
stdout,
|
||||
models.NewPackages(
|
||||
models.Package{
|
||||
Name: "SUSEConnect",
|
||||
NewVersion: "0.3.1",
|
||||
NewRelease: "19.11.2",
|
||||
Arch: "x86_64",
|
||||
},
|
||||
models.Package{
|
||||
Name: "SuSEfirewall2",
|
||||
NewVersion: "3.6.312",
|
||||
NewRelease: "2.10.1",
|
||||
Arch: "noarch",
|
||||
},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
packages, err := r.parseZypperLULines(tt.in)
|
||||
if err != nil {
|
||||
t.Errorf("Error has occurred, err: %s\ntt.in: %v", err, tt.in)
|
||||
return
|
||||
}
|
||||
for name, ePack := range tt.out {
|
||||
if !reflect.DeepEqual(ePack, packages[name]) {
|
||||
e := pp.Sprintf("%v", ePack)
|
||||
a := pp.Sprintf("%v", packages[name])
|
||||
t.Errorf("expected %s, actual %s", e, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanUpdatablePackage(t *testing.T) {
|
||||
r := newSUSE(config.ServerInfo{})
|
||||
r.Distro = config.Distro{Family: "sles"}
|
||||
stdout := `v | SLES12-SP2-Updates | SUSEConnect | 0.3.0-19.8.1 | 0.3.1-19.11.2 | x86_64`
|
||||
|
||||
var tests = []struct {
|
||||
in string
|
||||
out models.Package
|
||||
}{
|
||||
{
|
||||
stdout,
|
||||
models.Package{
|
||||
Name: "SUSEConnect",
|
||||
NewVersion: "0.3.1",
|
||||
NewRelease: "19.11.2",
|
||||
Arch: "x86_64",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
pack, err := r.parseZypperLUOneLine(tt.in)
|
||||
if err != nil {
|
||||
t.Errorf("Error has occurred, err: %s\ntt.in: %v", err, tt.in)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(*pack, tt.out) {
|
||||
e := pp.Sprintf("%v", tt.out)
|
||||
a := pp.Sprintf("%v", pack)
|
||||
t.Errorf("expected %s, actual %s", e, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
69
scan/utils.go
Normal file
69
scan/utils.go
Normal file
@@ -0,0 +1,69 @@
|
||||
/* 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"
|
||||
"strings"
|
||||
|
||||
"github.com/future-architect/vuls/config"
|
||||
"github.com/future-architect/vuls/models"
|
||||
"github.com/future-architect/vuls/util"
|
||||
)
|
||||
|
||||
func isRunningKernel(pack models.Package, family string, kernel models.Kernel) (isKernel, running bool) {
|
||||
switch family {
|
||||
case config.SUSEEnterpriseServer:
|
||||
if pack.Name == "kernel-default" {
|
||||
// Remove the last period and later because uname don't show that.
|
||||
ss := strings.Split(pack.Release, ".")
|
||||
rel := strings.Join(ss[0:len(ss)-1], ".")
|
||||
ver := fmt.Sprintf("%s-%s-default", pack.Version, rel)
|
||||
return true, kernel.Release == ver
|
||||
}
|
||||
return false, false
|
||||
|
||||
case config.RedHat, config.Oracle, config.CentOS, config.Amazon:
|
||||
if pack.Name == "kernel" {
|
||||
ver := fmt.Sprintf("%s-%s.%s", pack.Version, pack.Release, pack.Arch)
|
||||
return true, kernel.Release == ver
|
||||
}
|
||||
return false, false
|
||||
|
||||
default:
|
||||
util.Log.Warnf("Reboot required is not implemented yet: %s, %s", family, kernel)
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
|
||||
func rpmQa(distro config.Distro) string {
|
||||
const old = "rpm -qa --queryformat '%{NAME} %{EPOCH} %{VERSION} %{RELEASE} %{ARCH}\n'"
|
||||
const new = "rpm -qa --queryformat '%{NAME} %{EPOCHNUM} %{VERSION} %{RELEASE} %{ARCH}\n'"
|
||||
switch distro.Family {
|
||||
case config.SUSEEnterpriseServer:
|
||||
if v, _ := distro.MajorVersion(); v < 12 {
|
||||
return old
|
||||
}
|
||||
return new
|
||||
default:
|
||||
if v, _ := distro.MajorVersion(); v < 6 {
|
||||
return old
|
||||
}
|
||||
return new
|
||||
}
|
||||
}
|
||||
117
scan/utils_test.go
Normal file
117
scan/utils_test.go
Normal file
@@ -0,0 +1,117 @@
|
||||
/* 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 (
|
||||
"testing"
|
||||
|
||||
"github.com/future-architect/vuls/config"
|
||||
"github.com/future-architect/vuls/models"
|
||||
)
|
||||
|
||||
func TestIsRunningKernelSUSE(t *testing.T) {
|
||||
r := newSUSE(config.ServerInfo{})
|
||||
r.Distro = config.Distro{Family: config.SUSEEnterpriseServer}
|
||||
|
||||
kernel := models.Kernel{
|
||||
Release: "4.4.74-92.35-default",
|
||||
Version: "",
|
||||
}
|
||||
|
||||
var tests = []struct {
|
||||
pack models.Package
|
||||
family string
|
||||
kernel models.Kernel
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
pack: models.Package{
|
||||
Name: "kernel-default",
|
||||
Version: "4.4.74",
|
||||
Release: "92.35.1",
|
||||
Arch: "x86_64",
|
||||
},
|
||||
family: config.SUSEEnterpriseServer,
|
||||
kernel: kernel,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
pack: models.Package{
|
||||
Name: "kernel-default",
|
||||
Version: "4.4.59",
|
||||
Release: "92.20.2",
|
||||
Arch: "x86_64",
|
||||
},
|
||||
family: config.SUSEEnterpriseServer,
|
||||
kernel: kernel,
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
_, actual := isRunningKernel(tt.pack, tt.family, tt.kernel)
|
||||
if tt.expected != actual {
|
||||
t.Errorf("[%d] expected %t, actual %t", i, tt.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsRunningKernelRedHatLikeLinux(t *testing.T) {
|
||||
r := newRedhat(config.ServerInfo{})
|
||||
r.Distro = config.Distro{Family: config.Amazon}
|
||||
|
||||
kernel := models.Kernel{
|
||||
Release: "4.9.43-17.38.amzn1.x86_64",
|
||||
Version: "",
|
||||
}
|
||||
|
||||
var tests = []struct {
|
||||
pack models.Package
|
||||
family string
|
||||
kernel models.Kernel
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
pack: models.Package{
|
||||
Name: "kernel",
|
||||
Version: "4.9.43",
|
||||
Release: "17.38.amzn1",
|
||||
Arch: "x86_64",
|
||||
},
|
||||
family: config.Amazon,
|
||||
kernel: kernel,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
pack: models.Package{
|
||||
Name: "kernel",
|
||||
Version: "4.9.38",
|
||||
Release: "16.35.amzn1",
|
||||
Arch: "x86_64",
|
||||
},
|
||||
family: config.Amazon,
|
||||
kernel: kernel,
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
_, actual := isRunningKernel(tt.pack, tt.family, tt.kernel)
|
||||
if tt.expected != actual {
|
||||
t.Errorf("[%d] expected %t, actual %t", i, tt.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user